Compare commits

..

No commits in common. "d692165f99616667ac2c8f2b661b669ecb9b4dff" and "17edf6b911dab609c34de74af85613936d78c8bb" have entirely different histories.

69 changed files with 704 additions and 2233 deletions

View file

@ -27,10 +27,6 @@ clean: remove-podman-containers
cd backend && make clean cd backend && make clean
@echo "Cleaned up!" @echo "Cleaned up!"
.PHONY: itest
itest:
python testing.py
# Cleans up everything related to podman, not just the project. Make sure you understand what this means. # Cleans up everything related to podman, not just the project. Make sure you understand what this means.
podman-clean: podman-clean:
podman system reset --force podman system reset --force

View file

@ -118,7 +118,3 @@ uml: plantuml.jar
install-just: install-just:
@echo "Installing just" @echo "Installing just"
@curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
.PHONY: types
types:
tygo generate

View file

@ -2,8 +2,9 @@ package database
import ( import (
"embed" "embed"
"errors" "os"
"path/filepath" "path/filepath"
"time"
"ttime/internal/types" "ttime/internal/types"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -14,24 +15,20 @@ import (
type Database interface { type Database interface {
// Insert a new user into the database, password should be hashed before calling // Insert a new user into the database, password should be hashed before calling
AddUser(username string, password string) error AddUser(username string, password string) error
CheckUser(username string, password string) bool
RemoveUser(username string) error RemoveUser(username string) error
PromoteToAdmin(username string) error PromoteToAdmin(username string) error
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
Migrate() error Migrate(dirname string) error
GetProjectId(projectname string) (int, error) GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
ChangeUserRole(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error
GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersProject(projectname string) ([]UserProjectMember, error)
GetAllUsersApplication() ([]string, error) GetAllUsersApplication() ([]string, error)
GetProjectsForUser(username string) ([]types.Project, error) GetProjectsForUser(username string) ([]types.Project, error)
GetAllProjects() ([]types.Project, error) GetAllProjects() ([]types.Project, error)
GetProject(projectId int) (types.Project, error)
GetUserRole(username string, projectname string) (string, error) GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
@ -52,16 +49,27 @@ var scripts embed.FS
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?"
const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), const addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?),
ProjectLookup AS (SELECT id FROM projects WHERE name = ?) ProjectLookup AS (SELECT id FROM projects WHERE name = ?)
INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) INSERT INTO time_reports (project_id, user_id, start, end)
VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup), ?, ?);`
const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?"
const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id const getProjectsForUser = `
FROM projects JOIN user_roles ON projects.id = user_roles.project_id SELECT
JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;` projects.id,
projects.name,
projects.description,
projects.owner_user_id
FROM
projects
JOIN
user_roles ON projects.id = user_roles.project_id
JOIN
users ON user_roles.user_id = users.id
WHERE
users.username = ?;`
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) Database { func DbConnect(dbpath string) Database {
@ -80,42 +88,23 @@ func DbConnect(dbpath string) Database {
return &Db{db} return &Db{db}
} }
func (d *Db) CheckUser(username string, password string) bool {
var dbPassword string
err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username)
if err != nil {
return false
}
return dbPassword == password
}
// GetProjectsForUser retrieves all projects associated with a specific user.
func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) {
var projects []types.Project var projects []types.Project
err := d.Select(&projects, getProjectsForUser, username) err := d.Select(&projects, getProjectsForUser, username)
return projects, err return projects, err
} }
// GetAllProjects retrieves all projects from the database.
func (d *Db) GetAllProjects() ([]types.Project, error) { func (d *Db) GetAllProjects() ([]types.Project, error) {
var projects []types.Project var projects []types.Project
err := d.Select(&projects, "SELECT * FROM projects") err := d.Select(&projects, "SELECT * FROM projects")
return projects, err return projects, err
} }
// GetProject retrieves a specific project by its ID. func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP
func (d *Db) GetProject(projectId int) (types.Project, error) { _, err := d.Exec(addTimeReport, userName, projectName, start, end)
var project types.Project
err := d.Select(&project, "SELECT * FROM projects WHERE id = ?")
return project, err
}
func (d *Db) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error {
_, err := d.Exec(addWeeklyReport, userName, projectName, week, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime)
return err return err
} }
// AddUserToProject adds a user to a project with a specified role.
func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP
var userid int var userid int
userid, err := d.GetUserId(username) userid, err := d.GetUserId(username)
@ -133,28 +122,23 @@ func (d *Db) AddUserToProject(username string, projectname string, role string)
return err3 return err3
} }
// ChangeUserRole changes the role of a user within a project.
func (d *Db) ChangeUserRole(username string, projectname string, role string) error { func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
// Get the user ID
var userid int var userid int
userid, err := d.GetUserId(username) userid, err := d.GetUserId(username)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Get the project ID
var projectid int var projectid int
projectid, err2 := d.GetProjectId(projectname) projectid, err2 := d.GetProjectId(projectname)
if err2 != nil { if err2 != nil {
panic(err2) panic(err2)
} }
// Execute the SQL query to change the user's role
_, err3 := d.Exec(changeUserRole, role, userid, projectid) _, err3 := d.Exec(changeUserRole, role, userid, projectid)
return err3 return err3
} }
// GetUserRole retrieves the role of a user within a project.
func (d *Db) GetUserRole(username string, projectname string) (string, error) { func (d *Db) GetUserRole(username string, projectname string) (string, error) {
var role string var role string
err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname) err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname)
@ -259,74 +243,15 @@ func (d *Db) GetAllUsersApplication() ([]string, error) {
return usernames, nil return usernames, nil
} }
func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) {
var report types.WeeklyReport
query := `
SELECT
report_id,
user_id,
project_id,
week,
development_time,
meeting_time,
admin_time,
own_work_time,
study_time,
testing_time,
signed_by
FROM
weekly_reports
WHERE
user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
AND week = ?
`
err := d.Get(&report, query, username, projectName, week)
return report, err
}
// SignWeeklyReport signs a weekly report by updating the signed_by field
// with the provided project manager's ID, but only if the project manager
// is in the same project as the report
func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
if err != nil {
return err
}
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors.New("project manager doesn't have permission to sign the report")
}
// Update the signed_by field of the specified report
_, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId)
return err
}
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func (d *Db) Migrate() error { func (d *Db) Migrate(dirname string) error {
// Read the embedded scripts directory // Read the embedded scripts directory
files, err := scripts.ReadDir("migrations") files, err := scripts.ReadDir("migrations")
if err != nil { if err != nil {
return err return err
} }
if len(files) == 0 {
println("No migration files found")
return nil
}
tr := d.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
@ -336,7 +261,8 @@ func (d *Db) Migrate() error {
} }
// This is perhaps not the most elegant way to do this // This is perhaps not the most elegant way to do this
sqlBytes, err := scripts.ReadFile("migrations/" + file.Name()) sqlFile := filepath.Join("migrations", file.Name())
sqlBytes, err := os.ReadFile(sqlFile)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,15 +1,15 @@
package database package database
import ( import (
"fmt"
"testing" "testing"
"time"
) )
// Tests are not guaranteed to be sequential // Tests are not guaranteed to be sequential
func setupState() (Database, error) { func setupState() (Database, error) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
err := db.Migrate() err := db.Migrate("../../migrations")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,7 +93,7 @@ func TestPromoteToAdmin(t *testing.T) {
} }
} }
func TestAddWeeklyReport(t *testing.T) { func TestAddTimeReport(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
t.Error("setupState failed:", err) t.Error("setupState failed:", err)
@ -109,9 +109,12 @@ func TestAddWeeklyReport(t *testing.T) {
t.Error("AddProject failed:", err) t.Error("AddProject failed:", err)
} }
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) var now = time.Now()
var then = now.Add(time.Hour)
err = db.AddTimeReport("testproject", "testuser", now, then)
if err != nil { if err != nil {
t.Error("AddWeeklyReport failed:", err) t.Error("AddTimeReport failed:", err)
} }
} }
@ -131,9 +134,12 @@ func TestAddUserToProject(t *testing.T) {
t.Error("AddProject failed:", err) t.Error("AddProject failed:", err)
} }
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) var now = time.Now()
var then = now.Add(time.Hour)
err = db.AddTimeReport("testproject", "testuser", now, then)
if err != nil { if err != nil {
t.Error("AddWeeklyReport failed:", err) t.Error("AddTimeReport failed:", err)
} }
err = db.AddUserToProject("testuser", "testproject", "user") err = db.AddUserToProject("testuser", "testproject", "user")
@ -337,202 +343,3 @@ func TestGetProjectsForUser(t *testing.T) {
t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) t.Error("GetProjectsForUser failed: expected 1, got", len(projects))
} }
} }
func TestAddProject(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Retrieve the added project to verify its existence
projects, err := db.GetAllProjects()
if err != nil {
t.Error("GetAllProjects failed:", err)
}
// Check if the project was added successfully
found := false
for _, project := range projects {
if project.Name == "testproject" {
found = true
break
}
}
if !found {
t.Error("Added project not found")
}
}
func TestGetWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Check if the retrieved report matches the expected values
if report.UserId != 1 {
t.Errorf("Expected UserId to be 1, got %d", report.UserId)
}
if report.ProjectId != 1 {
t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId)
}
if report.Week != 1 {
t.Errorf("Expected Week to be 1, got %d", report.Week)
}
// Check other fields similarly
}
func TestSignWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add both regular users as members to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
err = db.AddUserToProject("projectManager", "testproject", "project_manager")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for one of the regular users
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Print project manager's ID
projectManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
fmt.Println("Project Manager's ID:", projectManagerID)
// Sign the report with the project manager
err = db.SignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("SignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's signed
signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is signed by the project manager
if *signedReport.SignedBy != projectManagerID {
t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy)
}
}
func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add the regular user as a member to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for the regular user
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
anotherManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
err = db.SignWeeklyReport(report.ReportId, anotherManagerID)
if err == nil {
t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't")
}
}

View file

@ -3,7 +3,7 @@
-- username is what is used for login -- username is what is used for login
-- password is the hashed password -- password is the hashed password
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY,
userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL password VARCHAR(255) NOT NULL

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL, description TEXT NOT NULL,
owner_user_id INTEGER NOT NULL, owner_user_id INTEGER NOT NULL,

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS time_reports (
id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
start DATETIME NOT NULL,
end DATETIME NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end
BEFORE INSERT ON time_reports
FOR EACH ROW
BEGIN
SELECT
CASE
WHEN NEW.start >= NEW.end THEN
RAISE (ABORT, 'start must be before end')
END;
END;

View file

@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS weekly_reports (
report_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
week INTEGER NOT NULL,
development_time INTEGER,
meeting_time INTEGER,
admin_time INTEGER,
own_work_time INTEGER,
study_time INTEGER,
testing_time INTEGER,
signed_by INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (project_id) REFERENCES projects(id),
FOREIGN KEY (signed_by) REFERENCES users(id)
);

View file

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS report_collection (
id INTEGER PRIMARY KEY,
owner_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
date DATE NOT NULL,
signed_by INTEGER, -- NULL if not signed
FOREIGN KEY (owner_id) REFERENCES users (id)
FOREIGN KEY (signed_by) REFERENCES users (id)
);

View file

@ -0,0 +1,16 @@
-- It is unclear weather this table will be used
-- Create the table to store hash salts
CREATE TABLE salts (
id INTEGER PRIMARY KEY,
salt TEXT NOT NULL
);
-- Commented out for now, no time for good practices, which is atrocious
-- Create a trigger to automatically generate a salt when inserting a new user record
-- CREATE TRIGGER generate_salt_trigger
-- AFTER INSERT ON users
-- BEGIN
-- INSERT INTO salts (salt) VALUES (randomblob(16));
-- UPDATE users SET salt_id = (SELECT last_insert_rowid()) WHERE id = new.id;
-- END;

View file

@ -1,9 +1,12 @@
package handlers package handlers
import ( import (
"time"
"ttime/internal/database" "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
// The actual interface that we will use // The actual interface that we will use
@ -14,9 +17,6 @@ type GlobalState interface {
LoginRenew(c *fiber.Ctx) error // To renew the token LoginRenew(c *fiber.Ctx) error // To renew the token
CreateProject(c *fiber.Ctx) error // To create a new project CreateProject(c *fiber.Ctx) error // To create a new project
GetUserProjects(c *fiber.Ctx) error // To get all projects GetUserProjects(c *fiber.Ctx) error // To get all projects
SubmitWeeklyReport(c *fiber.Ctx) error
GetWeeklyReport(c *fiber.Ctx) error
SignReport(c *fiber.Ctx) error
// GetProject(c *fiber.Ctx) error // To get a specific project // GetProject(c *fiber.Ctx) error // To get a specific project
// UpdateProject(c *fiber.Ctx) error // To update a project // UpdateProject(c *fiber.Ctx) error // To update a project
// DeleteProject(c *fiber.Ctx) error // To delete a project // DeleteProject(c *fiber.Ctx) error // To delete a project
@ -49,6 +49,45 @@ type GState struct {
ButtonCount int ButtonCount int
} }
// Register is a simple handler that registers a new user
//
// @Summary Register a new user
// @Description Register a new user
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /api/register [post]
func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
return c.Status(400).SendString(err.Error())
}
if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User added")
}
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
func (gs *GState) UserDelete(c *fiber.Ctx) error {
u := new(types.User)
if err := c.BodyParser(u); err != nil {
return c.Status(400).SendString(err.Error())
}
if err := gs.Db.RemoveUser(u.Username); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User deleted")
}
func (gs *GState) GetButtonCount(c *fiber.Ctx) error { func (gs *GState) GetButtonCount(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
} }
@ -57,3 +96,132 @@ func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error {
gs.ButtonCount++ gs.ButtonCount++
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
} }
// Login is a simple login handler that returns a JWT token
func (gs *GState) Login(c *fiber.Ctx) error {
// To test: curl --data "user=user&pass=pass" http://localhost:8080/api/login
user := c.FormValue("user")
pass := c.FormValue("pass")
// Throws Unauthorized error
if user != "user" || pass != "pass" {
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": user,
"admin": false,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// LoginRenew is a simple handler that renews the token
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// CreateProject is a simple handler that creates a new project
func (gs *GState) CreateProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
p := new(types.NewProject)
if err := c.BodyParser(p); err != nil {
return c.Status(400).SendString(err.Error())
}
// Get the username from the token and set it as the owner of the project
// This is ugly but
claims := user.Claims.(jwt.MapClaims)
p.Owner = claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}
// GetUserProjects returns all projects that the user is a member of
func (gs *GState) GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Then dip into the database to get the projects
projects, err := gs.Db.GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}
// ListAllUsers is a handler that returns a list of all users in the application database
func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := gs.Db.GetAllUsersApplication()
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}
// ProjectRoleChange is a handler that changes a user's role within a project
func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
username := c.Params("username")
projectName := c.Params("projectName")
role := c.Params("role")
// Change the user's role within the project in the database
if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,98 +0,0 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// CreateProject is a simple handler that creates a new project
func (gs *GState) CreateProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
p := new(types.NewProject)
if err := c.BodyParser(p); err != nil {
return c.Status(400).SendString(err.Error())
}
// Get the username from the token and set it as the owner of the project
// This is ugly but
claims := user.Claims.(jwt.MapClaims)
owner := claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}
// GetUserProjects returns all projects that the user is a member of
func (gs *GState) GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Then dip into the database to get the projects
projects, err := gs.Db.GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}
// ProjectRoleChange is a handler that changes a user's role within a project
func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
username := c.Params("username")
projectName := c.Params("projectName")
role := c.Params("role")
// Change the user's role within the project in the database
if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}
// GetProject retrieves a specific project by its ID
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
return c.JSON(project)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -1,93 +0,0 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil {
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 {
return c.Status(400).SendString("Invalid week number")
}
if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 {
return c.Status(400).SendString("Invalid time report")
}
if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Time report added")
}
// Handler for retrieving weekly report
func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
println("GetWeeklyReport")
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name and week from query parameters
projectName := c.Query("projectName")
println(projectName)
week := c.Query("week")
println(week)
// Convert week to integer
weekInt, err := strconv.Atoi(week)
if err != nil {
return c.Status(400).SendString("Invalid week number")
}
// Call the database function to get the weekly report
report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the retrieved weekly report
return c.JSON(report)
}
func (gs *GState) SignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
managerUsername := claims["name"].(string)
// Extract the report ID and project manager ID from request parameters
reportID, err := strconv.Atoi(c.Params("reportId"))
if err != nil {
return c.Status(400).SendString("Invalid report ID")
}
// Call the database function to get the project manager ID
managerID, err := gs.Db.GetUserId(managerUsername)
if err != nil {
return c.Status(500).SendString("Failed to get project manager ID")
}
// Call the database function to sign the weekly report
err = gs.Db.SignWeeklyReport(reportID, managerID)
if err != nil {
return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error())
}
// Return success response
return c.Status(200).SendString("Weekly report signed successfully")
}

View file

@ -1,124 +0,0 @@
package handlers
import (
"time"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// Register is a simple handler that registers a new user
//
// @Summary Register a new user
// @Description Register a new user
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /api/register [post]
func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
println("Error parsing body")
return c.Status(400).SendString(err.Error())
}
println("Adding user:", u.Username)
if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
return c.Status(500).SendString(err.Error())
}
println("User added:", u.Username)
return c.Status(200).SendString("User added")
}
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")
// Read username from Locals
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username != auth_username {
return c.Status(403).SendString("You can only delete yourself")
}
if err := gs.Db.RemoveUser(username); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User deleted")
}
// Login is a simple login handler that returns a JWT token
func (gs *GState) Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
println("Error parsing body")
return c.Status(400).SendString(err.Error())
}
println("Username:", u.Username)
if !gs.Db.CheckUser(u.Username, u.Password) {
println("User not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": u.Username,
"admin": false,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
println("Token created for user:", u.Username)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
println("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
println("Successfully signed token for user:", u.Username)
return c.JSON(fiber.Map{"token": t})
}
// LoginRenew is a simple handler that renews the token
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// ListAllUsers is a handler that returns a list of all users in the application database
func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := gs.Db.GetAllUsersApplication()
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -1,46 +0,0 @@
package types
// This is what should be submitted to the server, the username will be derived from the JWT token
type NewWeeklyReport struct {
// The name of the project, as it appears in the database
ProjectName string `json:"projectName"`
// The week number
Week int `json:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime"`
// Total time spent on studying
StudyTime int `json:"studyTime"`
// Total time spent on testing
TestingTime int `json:"testingTime"`
}
type WeeklyReport struct {
// The ID of the report
ReportId int `json:"reportId" db:"report_id"`
// The user id of the user who submitted the report
UserId int `json:"userId" db:"user_id"`
// The name of the project, as it appears in the database
ProjectId int `json:"projectId" db:"project_id"`
// The week number
Week int `json:"week" db:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime" db:"development_time"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime" db:"meeting_time"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime" db:"admin_time"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"`
// Total time spent on studying
StudyTime int `json:"studyTime" db:"study_time"`
// Total time spent on testing
TestingTime int `json:"testingTime" db:"testing_time"`
// The project manager who signed it
SignedBy *int `json:"signedBy" db:"signed_by"`
}

View file

@ -8,8 +8,9 @@ type Project struct {
Owner string `json:"owner" db:"owner_user_id"` Owner string `json:"owner" db:"owner_user_id"`
} }
// As it arrives from the client, Owner is derived from the JWT token // As it arrives from the client
type NewProject struct { type NewProject struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Owner string `json:"owner"`
} }

View file

@ -16,7 +16,6 @@ func (u *User) ToPublicUser() (*PublicUser, error) {
}, nil }, nil
} }
// Should be used when registering, for example
type NewUser struct { type NewUser struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`

View file

@ -43,11 +43,6 @@ func main() {
// Connect to the database // Connect to the database
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database
if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err)
}
// Get our global state // Get our global state
gs := handlers.NewGlobalState(db) gs := handlers.NewGlobalState(db)
// Create the server // Create the server
@ -73,12 +68,10 @@ func main() {
SigningKey: jwtware.SigningKey{Key: []byte("secret")}, SigningKey: jwtware.SigningKey{Key: []byte("secret")},
})) }))
server.Post("/api/submitReport", gs.SubmitWeeklyReport)
server.Get("/api/getUserProjects", gs.GetUserProjects) server.Get("/api/getUserProjects", gs.GetUserProjects)
server.Post("/api/loginrenew", gs.LoginRenew) server.Post("/api/loginrenew", gs.LoginRenew)
server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches
server.Post("/api/project", gs.CreateProject) server.Post("/api/project", gs.CreateProject)
server.Get("/api/getWeeklyReport", gs.GetWeeklyReport)
// Announce the port we are listening on and start the server // Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port)) err = server.Listen(fmt.Sprintf(":%d", conf.Port))

View file

@ -1,9 +0,0 @@
packages:
- path: "ttime/internal/types"
output_path: "../frontend/src/Types/goTypes.ts"
type_mappings:
time.Time: "string /* RFC3339 */"
null.String: "null | string"
null.Bool: "null | boolean"
uuid.UUID: "string /* uuid */"
uuid.NullUUID: "null | string /* uuid */"

View file

@ -9,7 +9,7 @@ module.exports = {
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
], ],
ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs', 'goTypes.ts'], ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['react-refresh', 'prettier'], plugins: ['react-refresh', 'prettier'],
rules: { rules: {

View file

@ -1,256 +1,57 @@
import { import { NewProject, Project } from "../Types/Project";
NewWeeklyReport, import { NewUser, User } from "../Types/Users";
NewUser,
User,
Project,
NewProject,
} from "../Types/goTypes";
// This type of pattern should be hard to misuse
export interface APIResponse<T> {
success: boolean;
message?: string;
data?: T;
}
// Note that all protected routes also require a token
// Defines all the methods that an instance of the API must implement // Defines all the methods that an instance of the API must implement
interface API { interface API {
/** Register a new user */ /** Register a new user */
registerUser(user: NewUser): Promise<APIResponse<User>>; registerUser(user: NewUser): Promise<User>;
/** Remove a user */ /** Remove a user */
removeUser(username: string, token: string): Promise<APIResponse<User>>; removeUser(username: string): Promise<User>;
/** Login */
login(NewUser: NewUser): Promise<APIResponse<string>>;
/** Renew the token */
renewToken(token: string): Promise<APIResponse<string>>;
/** Create a project */ /** Create a project */
createProject( createProject(project: NewProject): Promise<Project>;
project: NewProject, /** Renew the token */
token: string, renewToken(token: string): Promise<string>;
): Promise<APIResponse<Project>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Submit a weekly report */
submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<NewWeeklyReport>>;
/**Gets a weekly report*/
getWeeklyReport(
username: string,
projectName: string,
week: string,
token: string,
): Promise<APIResponse<NewWeeklyReport>>;
} }
// Export an instance of the API // Export an instance of the API
export const api: API = { export const api: API = {
async registerUser(user: NewUser): Promise<APIResponse<User>> { async registerUser(user: NewUser): Promise<User> {
try { return fetch("/api/register", {
const response = await fetch("/api/register", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(user), body: JSON.stringify(user),
}); }).then((res) => res.json() as Promise<User>);
if (!response.ok) {
return {
success: false,
message: "Failed to register user: " + response.status,
};
} else {
// const data = (await response.json()) as User; // The API does not currently return the user
return { success: true };
}
} catch (e) {
return {
success: false,
message: "Unknown error while registering user",
};
}
}, },
async removeUser( async removeUser(username: string): Promise<User> {
username: string, return fetch("/api/userdelete", {
token: string,
): Promise<APIResponse<User>> {
try {
const response = await fetch("/api/userdelete", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}, },
body: JSON.stringify(username), body: JSON.stringify(username),
}); }).then((res) => res.json() as Promise<User>);
if (!response.ok) {
return { success: false, message: "Failed to remove user" };
} else {
const data = (await response.json()) as User;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to remove user" };
}
}, },
async createProject( async createProject(project: NewProject): Promise<Project> {
project: NewProject, return fetch("/api/project", {
token: string,
): Promise<APIResponse<Project>> {
try {
const response = await fetch("/api/project", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}, },
body: JSON.stringify(project), body: JSON.stringify(project),
}); }).then((res) => res.json() as Promise<Project>);
if (!response.ok) {
return { success: false, message: "Failed to create project" };
} else {
const data = (await response.json()) as Project;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to create project" };
}
}, },
async renewToken(token: string): Promise<APIResponse<string>> { async renewToken(token: string): Promise<string> {
try { return fetch("/api/loginrenew", {
const response = await fetch("/api/loginrenew", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}, },
}); }).then((res) => res.json() as Promise<string>);
if (!response.ok) {
return { success: false, message: "Failed to renew token" };
} else {
const data = (await response.json()) as string;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to renew token" };
}
},
async getUserProjects(token: string): Promise<APIResponse<Project[]>> {
try {
const response = await fetch("/api/getUserProjects", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
} else {
const data = (await response.json()) as Project[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
}
},
async submitWeeklyReport(
weeklyReport: NewWeeklyReport,
token: string,
): Promise<APIResponse<NewWeeklyReport>> {
try {
const response = await fetch("/api/submitWeeklyReport", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(weeklyReport),
});
if (!response.ok) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
const data = (await response.json()) as NewWeeklyReport;
return { success: true, data };
} catch (e) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
},
async getWeeklyReport(
username: string,
projectName: string,
week: string,
token: string,
): Promise<APIResponse<NewWeeklyReport>> {
try {
const response = await fetch("/api/getWeeklyReport", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ username, projectName, week }),
});
if (!response.ok) {
return { success: false, message: "Failed to get weekly report" };
} else {
const data = (await response.json()) as NewWeeklyReport;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get weekly report" };
}
},
async login(NewUser: NewUser): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(NewUser),
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
} else {
const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token };
}
} catch (e) {
return Promise.resolve({ success: false, message: "Failed to login" });
}
}, },
}; };

View file

@ -1,94 +0,0 @@
import { useState } from "react";
import { APIResponse, api } from "../API/API";
import { NewProject, Project } from "../Types/goTypes";
import InputField from "./InputField";
import Logo from "../assets/Logo.svg";
import Button from "./Button";
/**
* Tries to add a project to the system
* @param props - Project name and description
* @returns {boolean} True if created, false if not
*/
function CreateProject(props: { name: string; description: string }): boolean {
const project: NewProject = {
name: props.name,
description: props.description,
};
let created = false;
api
.createProject(project, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<Project>) => {
if (response.success) {
created = true;
} else {
console.error(response.message);
}
})
.catch((error) => {
console.error("An error occurred during creation:", error);
});
return created;
}
/**
* Tries to add a project to the system
* @returns {JSX.Element} UI for project adding
*/
function AddProject(): JSX.Element {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
return (
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => {
e.preventDefault();
CreateProject({ name: name, description: description });
}}
>
<img
src={Logo}
className="logo w-[7vw] mb-10 mt-10"
alt="TTIME Logo"
/>
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project
</h3>
<InputField
label="Name"
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
<InputField
label="Description"
type="text"
value={description}
onChange={(e) => {
setDescription(e.target.value);
}}
/>
<div className="flex items-center justify-between">
<Button
text="Create"
onClick={(): void => {
return;
}}
type="submit"
/>
</div>
</form>
<p className="text-center text-gray-500 text-xs"></p>
</div>
</div>
);
}
export default AddProject;

View file

@ -1,24 +0,0 @@
import { useEffect } from "react";
const BackgroundAnimation = (): JSX.Element => {
useEffect(() => {
const images = [
"src/assets/1.jpg",
"src/assets/2.jpg",
"src/assets/3.jpg",
"src/assets/4.jpg",
];
// Pre-load images
for (const i of images) {
console.log(i);
}
// Start animation
document.body.style.animation = "backgroundTransition 30s infinite";
}, []);
return <></>;
};
export default BackgroundAnimation;

View file

@ -2,15 +2,17 @@ import Header from "./Header";
import Footer from "./Footer"; import Footer from "./Footer";
function BasicWindow({ function BasicWindow({
username,
content, content,
buttons, buttons,
}: { }: {
username: string;
content: React.ReactNode; content: React.ReactNode;
buttons: React.ReactNode; buttons: React.ReactNode;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className="font-sans flex flex-col h-screen bg-white border-2 border-black overflow-auto pt-[110px]"> <div className="font-sans flex flex-col h-screen bg-white border-2 border-black overflow-auto pt-[110px]">
<Header /> <Header username={username} />
<div className="flex flex-col items-center flex-grow">{content}</div> <div className="flex flex-col items-center flex-grow">{content}</div>
<Footer>{buttons}</Footer> <Footer>{buttons}</Footer>
</div> </div>

View file

@ -1,17 +1,14 @@
function Button({ function Button({
text, text,
onClick, onClick,
type,
}: { }: {
text: string; text: string;
onClick: () => void; onClick: () => void;
type: "submit" | "button" | "reset";
}): JSX.Element { }): JSX.Element {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;" className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;"
type={type}
> >
{text} {text}
</button> </button>

View file

@ -1,247 +0,0 @@
import { useState, useEffect } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate } from "react-router-dom";
import Button from "./Button";
export default function GetWeeklyReport(): JSX.Element {
const [projectName, setProjectName] = useState("");
const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0);
const [adminTime, setAdminTime] = useState(0);
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const token = localStorage.getItem("accessToken") ?? "";
const username = localStorage.getItem("username") ?? "";
useEffect(() => {
const fetchWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport(
username,
projectName,
week.toString(),
token,
);
if (response.success) {
const report: NewWeeklyReport = response.data ?? {
projectName: "",
week: 0,
developmentTime: 0,
meetingTime: 0,
adminTime: 0,
ownWorkTime: 0,
studyTime: 0,
testingTime: 0,
};
setProjectName(report.projectName);
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
setAdminTime(report.adminTime);
setOwnWorkTime(report.ownWorkTime);
setStudyTime(report.studyTime);
setTestingTime(report.testingTime);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
};
fetchWeeklyReport();
}, []);
const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
projectName,
week,
developmentTime,
meetingTime,
adminTime,
ownWorkTime,
studyTime,
testingTime,
};
await api.submitWeeklyReport(newWeeklyReport, token);
};
const navigate = useNavigate();
return (
<>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form
onSubmit={(e) => {
if (week === 0) {
alert("Please enter a week number");
e.preventDefault();
return;
}
e.preventDefault();
void handleNewWeeklyReport();
navigate("/project");
}}
>
<div className="flex flex-col items-center">
<input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week"
placeholder="Week"
value={
week === 0 ? "" : `2024-W${week.toString().padStart(2, "0")}`
}
onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]);
setWeek(weekNumber);
}}
onKeyDown={(event) => {
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();
}}
/>
<table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead>
<tr>
<th className="w-1/2 py-2 border-b-2 border-black">
Activity
</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
<tr className="h-[10vh]">
<td>Development</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime}
onChange={(e) => {
setDevelopmentTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime}
onChange={(e) => {
setMeetingTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime}
onChange={(e) => {
setAdminTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime}
onChange={(e) => {
setOwnWorkTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime}
onChange={(e) => {
setStudyTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime}
onChange={(e) => {
setTestingTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
</tbody>
</table>
<Button
text="Submit"
onClick={(): void => {
return;
}}
type="submit"
/>
</div>
</form>
</div>
</>
);
}

View file

@ -1,11 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
function Header(): JSX.Element { function Header({ username }: { username: string }): JSX.Element {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const handleLogout = (): void => { const handleLogout = (): void => {
localStorage.clear(); // Add any logout logic here
}; };
return ( return (
@ -31,7 +31,7 @@ function Header(): JSX.Element {
}} }}
> >
<button className="mr-4 underline font-bold text-white"> <button className="mr-4 underline font-bold text-white">
{localStorage.getItem("username")} {username}
</button> </button>
{isOpen && ( {isOpen && (

View file

@ -1,41 +0,0 @@
/**
* A customizable input field
* @param props - Settings for the field
* @returns {JSX.Element} The input field
* @example
* <InputField
* type="text"
* label="Example"
* onChange={(e) => {
* setExample(e.target.value);
* }}
* value={example}
* />
*/
function InputField(props: {
label: string;
type: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor={props.label}
>
{props.label}
</label>
<input
className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id={props.label}
type={props.type}
placeholder={props.label}
value={props.value}
onChange={props.onChange}
/>
</div>
);
}
export default InputField;

View file

@ -1,58 +0,0 @@
import { NewUser } from "../Types/goTypes";
import { api, APIResponse } from "../API/API";
import { Dispatch, SetStateAction } from "react";
/*
* Checks if user is in database with api.login and then sets proper authority level
* TODO: change so that it checks for user type (admin, user, pm) somehow instead
**/
function LoginCheck(props: {
username: string;
password: string;
setAuthority: Dispatch<SetStateAction<number>>;
}): void {
const user: NewUser = {
username: props.username,
password: props.password,
};
localStorage.clear();
api
.login(user)
.then((response: APIResponse<string>) => {
if (response.success) {
if (response.data !== undefined) {
const token = response.data;
localStorage.setItem("accessToken", token);
localStorage.setItem("username", props.username);
//TODO: change so that it checks for user type (admin, user, pm) instead
if (token !== "" && props.username === "admin") {
props.setAuthority((prevAuth) => {
prevAuth = 1;
return prevAuth;
});
} else if (token !== "" && props.username === "pm") {
props.setAuthority((prevAuth) => {
prevAuth = 2;
return prevAuth;
});
} else if (token !== "" && props.username === "user") {
props.setAuthority((prevAuth) => {
prevAuth = 3;
return prevAuth;
});
}
} else {
console.error("Token was undefined");
}
} else {
console.error("Token could not be fetched/No such user");
}
})
.catch((error) => {
console.error("An error occurred during login:", error);
});
}
export default LoginCheck;

View file

@ -1,55 +0,0 @@
import { Dispatch, FormEventHandler, SetStateAction } from "react";
import Button from "./Button";
import InputField from "./InputField";
/**
* A login field complete with input fields
* and a button for submitting the information
* @param props - Settings
* @returns {JSX.Element} A login component
* @example
* <Login
* handleSubmit={handleSubmit}
* setUsername={setUsername}
* setPassword={setPassword}
* username={username}
* password={password}
* />
*/
function Login(props: {
handleSubmit: FormEventHandler<HTMLFormElement>;
setUsername: Dispatch<SetStateAction<string>>;
setPassword: Dispatch<SetStateAction<string>>;
username: string;
password: string;
}): JSX.Element {
return (
<form className="flex flex-col items-center" onSubmit={props.handleSubmit}>
<InputField
type="text"
label="Username"
onChange={(e) => {
props.setUsername(e.target.value);
}}
value={props.username}
/>
<InputField
type="password"
label="Password"
onChange={(e) => {
props.setPassword(e.target.value);
}}
value={props.password}
/>
<Button
text="Login"
onClick={(): void => {
return;
}}
type={"submit"}
/>
</form>
);
}
export default Login;

View file

@ -1,208 +0,0 @@
import { useState, useContext } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate } from "react-router-dom";
import Button from "./Button";
import { ProjectNameContext } from "../Pages/YourProjectsPage";
export default function NewWeeklyReport(): JSX.Element {
const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0);
const [adminTime, setAdminTime] = useState(0);
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const projectName = useContext(ProjectNameContext);
const token = localStorage.getItem("accessToken") ?? "";
const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
projectName,
week,
developmentTime,
meetingTime,
adminTime,
ownWorkTime,
studyTime,
testingTime,
};
await api.submitWeeklyReport(newWeeklyReport, token);
};
const navigate = useNavigate();
return (
<>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form
onSubmit={(e) => {
if (week === 0) {
alert("Please enter a week number");
e.preventDefault();
return;
}
e.preventDefault();
void handleNewWeeklyReport();
navigate("/project");
}}
>
<div className="flex flex-col items-center">
<input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week"
placeholder="Week"
onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]);
setWeek(weekNumber);
}}
onKeyDown={(event) => {
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();
}}
/>
<table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead>
<tr>
<th className="w-1/2 py-2 border-b-2 border-black">
Activity
</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
<tr className="h-[10vh]">
<td>Development</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime}
onChange={(e) => {
setDevelopmentTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime}
onChange={(e) => {
setMeetingTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime}
onChange={(e) => {
setAdminTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime}
onChange={(e) => {
setOwnWorkTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime}
onChange={(e) => {
setStudyTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime}
onChange={(e) => {
setTestingTime(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
</tbody>
</table>
<Button
text="Submit"
onClick={(): void => {
return;
}}
type="submit"
/>
</div>
</form>
</div>
</>
);
}

View file

@ -1,5 +1,5 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Project } from "../Types/goTypes"; import { Project } from "../Types/Project";
/** /**
* The props for the ProjectsProps component * The props for the ProjectsProps component

View file

@ -1,74 +1,70 @@
import { useState } from "react"; import { useState } from "react";
import { NewUser } from "../Types/goTypes"; import { NewUser } from "../Types/Users";
import { api } from "../API/API"; import { api } from "../API/API";
import Logo from "../assets/Logo.svg";
import Button from "./Button";
import InputField from "./InputField";
import { useNavigate } from "react-router-dom";
export default function Register(): JSX.Element { export default function Register(): JSX.Element {
const [username, setUsername] = useState<string>(); const [username, setUsername] = useState("");
const [password, setPassword] = useState<string>(); const [password, setPassword] = useState("");
const [errMessage, setErrMessage] = useState<string>();
const nav = useNavigate();
const handleRegister = async (): Promise<void> => { const handleRegister = async (): Promise<void> => {
const newUser: NewUser = { const newUser: NewUser = { userName: username, password };
username: username ?? "", await api.registerUser(newUser); // TODO: Handle errors
password: password ?? "",
};
const response = await api.registerUser(newUser);
if (response.success) {
nav("/"); // Instantly navigate to the login page
} else {
setErrMessage(response.message ?? "Unknown error");
console.error(errMessage);
}
}; };
return ( return (
<div className="flex flex-col h-fit w-screen items-center justify-center"> <div>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20"> <div className="w-full max-w-xs">
<form <form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void handleRegister(); void handleRegister();
}} }}
> >
<img <h3 className="pb-2">Register new user</h3>
src={Logo} <div className="mb-4">
className="logo w-[7vw] mb-10 mt-10" <label
alt="TTIME Logo" className="block text-gray-700 text-sm font-bold mb-2"
/> htmlFor="username"
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> >
Register New User Username
</h3> </label>
<InputField <input
label="Username" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text" type="text"
placeholder="Username"
value={username} value={username}
onChange={(e) => { onChange={(e) => {
setUsername(e.target.value); setUsername(e.target.value);
}} }}
/> />
<InputField </div>
label="Password" <div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Password
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password" type="password"
placeholder="Choose your password"
value={password} value={password}
onChange={(e) => { onChange={(e) => {
setPassword(e.target.value); setPassword(e.target.value);
}} }}
/> />
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <button
text="Register" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
onClick={(): void => {
return;
}}
type="submit" type="submit"
/> >
Register
</button>
</div> </div>
</form> </form>
<p className="text-center text-gray-500 text-xs"></p> <p className="text-center text-gray-500 text-xs"></p>

View file

@ -0,0 +1,59 @@
function NewTimeReport(): JSX.Element {
const activities = [
"Development",
"Meeting",
"Administration",
"Own Work",
"Studies",
"Testing",
];
return (
<>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week"
placeholder="Week"
onKeyDown={(event) => {
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();
}}
/>
<table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead>
<tr>
<th className="w-1/2 py-2 border-b-2 border-black">Activity</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
{activities.map((activity, index) => (
<tr key={index} className="h-[10vh]">
<td>{activity}</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
}
export default NewTimeReport;

View file

@ -1,11 +1,11 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { PublicUser } from "../Types/goTypes"; import { User } from "../Types/Users";
/** /**
* The props for the UserProps component * The props for the UserProps component
*/ */
interface UserProps { interface UserProps {
users: PublicUser[]; users: User[];
} }
/** /**
@ -23,9 +23,9 @@ export function UserListAdmin(props: UserProps): JSX.Element {
<div> <div>
<ul className="font-bold underline text-[30px] cursor-pointer padding"> <ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => ( {props.users.map((user) => (
<Link to="/adminUserInfo" key={user.userId} state={user.username}> <Link to="/admin-view-user" key={user.id} state={user.userName}>
<li className="pt-5" key={user.userId}> <li className="pt-5" key={user.id}>
{user.username} {user.userName}
</li> </li>
</Link> </Link>
))} ))}

View file

@ -1,43 +0,0 @@
import React, { useEffect, useState } from "react";
import { api } from "../API/API";
import { Project } from "../Types/goTypes";
const UserProjectListAdmin: React.FC = () => {
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
const fetchProjects = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const username = getUsernameFromContext(); // Assuming you have a function to get the username from your context
const response = await api.getUserProjects(username, token);
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
void fetchProjects();
}, []);
return (
<div>
<h2>User Projects</h2>
<ul>
{projects.map((project) => (
<li key={project.id}>
<span>{project.name}</span>
{/* Add any additional project details you want to display */}
</li>
))}
</ul>
</div>
);
};
export default UserProjectListAdmin;

View file

@ -1,16 +1,26 @@
import AddProject from "../../Components/AddProject";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
function AdminAddProject(): JSX.Element { function AdminAddProject(): JSX.Element {
const content = <AddProject />; const content = <></>;
const buttons = ( const buttons = (
<> <>
<BackButton /> <Button
text="Finish"
onClick={(): void => {
return;
}}
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
/>
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminAddProject; export default AdminAddProject;

View file

@ -1,20 +1,26 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Register from "../../Components/Register"; import Button from "../../Components/Button";
function AdminAddUser(): JSX.Element { function AdminAddUser(): JSX.Element {
const content = ( const content = <></>;
<>
<Register />
</>
);
const buttons = ( const buttons = (
<> <>
<BackButton /> <Button
text="Finish"
onClick={(): void => {
return;
}}
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
/>
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminAddUser; export default AdminAddUser;

View file

@ -11,18 +11,16 @@ function AdminChangeUsername(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminChangeUsername; export default AdminChangeUsername;

View file

@ -1,5 +1,3 @@
import { Link } from "react-router-dom";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -8,19 +6,21 @@ function AdminManageProjects(): JSX.Element {
const buttons = ( const buttons = (
<> <>
<Link to="/addProject">
<Button <Button
text="Add Project" text="Add Project"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</Link> <Button
<BackButton /> text="Back"
onClick={(): void => {
return;
}}
/>
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminManageProjects; export default AdminManageProjects;

View file

@ -2,14 +2,14 @@ import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import BackButton from "../../Components/BackButton"; import BackButton from "../../Components/BackButton";
import { UserListAdmin } from "../../Components/UserListAdmin"; import { UserListAdmin } from "../../Components/UserListAdmin";
import { PublicUser } from "../../Types/goTypes"; import { User } from "../../Types/Users";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
function AdminManageUsers(): JSX.Element { function AdminManageUsers(): JSX.Element {
//TODO: Change so that it reads users from database //TODO: Change so that it reads users from database
const users: PublicUser[] = []; const users: User[] = [];
for (let i = 1; i <= 20; i++) { for (let i = 1; i <= 20; i++) {
users.push({ userId: "id" + i, username: "Example User " + i }); users.push({ id: i, userName: "Example User " + i });
} }
const navigate = useNavigate(); const navigate = useNavigate();
@ -28,14 +28,13 @@ function AdminManageUsers(): JSX.Element {
<Button <Button
text="Add User" text="Add User"
onClick={(): void => { onClick={(): void => {
navigate("/adminAddUser"); navigate("/admin-add-user");
}} }}
type="button"
/> />
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminManageUsers; export default AdminManageUsers;

View file

@ -6,12 +6,12 @@ function AdminMenuPage(): JSX.Element {
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Administrator Menu</h1> <h1 className="font-bold text-[30px] mb-[20px]">Administrator Menu</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<Link to="/adminManageUser"> <Link to="/admin-manage-users">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Manage Users Manage Users
</h1> </h1>
</Link> </Link>
<Link to="/adminManageProject"> <Link to="/admin-manage-projects">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Manage Projects Manage Projects
</h1> </h1>
@ -22,6 +22,6 @@ function AdminMenuPage(): JSX.Element {
const buttons = <></>; const buttons = <></>;
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminMenuPage; export default AdminMenuPage;

View file

@ -11,18 +11,16 @@ function AdminProjectAddMember(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectAddMember; export default AdminProjectAddMember;

View file

@ -11,18 +11,16 @@ function AdminProjectChangeUserRole(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectChangeUserRole; export default AdminProjectChangeUserRole;

View file

@ -11,18 +11,16 @@ function AdminProjectManageMembers(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectManageMembers; export default AdminProjectManageMembers;

View file

@ -11,18 +11,16 @@ function AdminProjectPage(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectPage; export default AdminProjectPage;

View file

@ -11,11 +11,10 @@ function AdminProjectStatistics(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectStatistics; export default AdminProjectStatistics;

View file

@ -11,18 +11,16 @@ function AdminProjectViewMemberInfo(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminProjectViewMemberInfo; export default AdminProjectViewMemberInfo;

View file

@ -1,12 +1,15 @@
import { useLocation } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import BackButton from "../../Components/BackButton"; import BackButton from "../../Components/BackButton";
import UserProjectListAdmin from "../../Components/UserProjectListAdmin";
function AdminViewUserInfo(): JSX.Element { function AdminViewUserInfo(): JSX.Element {
const content = ( const content = (
<> <>
<UserProjectListAdmin /> <h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<p>Put relevant info on user from database here</p>
</div>
</> </>
); );
@ -17,12 +20,11 @@ function AdminViewUserInfo(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default AdminViewUserInfo; export default AdminViewUserInfo;

View file

@ -1,23 +0,0 @@
import { useState, useEffect } from "react";
import LoginPage from "./LoginPage";
import { useNavigate } from "react-router-dom";
function App(): JSX.Element {
const navigate = useNavigate();
const [authority, setAuthority] = useState(0);
useEffect(() => {
if (authority === 1) {
navigate("/admin");
} else if (authority === 2) {
navigate("/pm");
} else if (authority === 3) {
navigate("/user");
}
}, [authority, navigate]);
return <LoginPage setAuthority={setAuthority} />;
}
export default App;

View file

@ -1,30 +1,71 @@
import Button from "../Components/Button";
import Logo from "/src/assets/Logo.svg"; import Logo from "/src/assets/Logo.svg";
import "./LoginPage.css"; import "./LoginPage.css";
import { Dispatch, FormEvent, SetStateAction, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import BackgroundAnimation from "../Components/BackgroundAnimation"; import { NewUser } from "../Types/Users";
import LoginField from "../Components/LoginField"; import { useNavigate } from "react-router-dom";
import LoginCheck from "../Components/LoginCheck";
function LoginPage(props: { const PreloadBackgroundAnimation = (): JSX.Element => {
setAuthority: Dispatch<SetStateAction<number>>; useEffect(() => {
}): JSX.Element { const images = [
const [username, setUsername] = useState(""); "src/assets/1.jpg",
const [password, setPassword] = useState(""); "src/assets/2.jpg",
"src/assets/3.jpg",
"src/assets/4.jpg",
];
// Pre-load images
for (const i of images) {
console.log(i);
}
// Start animation
document.body.style.animation = "backgroundTransition 30s infinite";
}, []);
return <></>;
};
function LoginPage(): JSX.Element {
//Example users for testing without backend, remove when using backend
const admin: NewUser = {
userName: "admin",
password: "123",
};
const pmanager: NewUser = {
userName: "pmanager",
password: "123",
};
const user: NewUser = {
userName: "user",
password: "123",
};
const navigate = useNavigate();
/* On submit (enter or button click) check if username and password match any user /* On submit (enter or button click) check if username and password match any user
and if so, redirect to correct page */ and if so, redirect to correct page */
function handleSubmit(event: FormEvent<HTMLFormElement>): void { function handleSubmit(event: FormEvent<HTMLFormElement>): void {
event.preventDefault(); event.preventDefault();
LoginCheck({ //TODO: Compare with db instead when finished
username: username, if (username === admin.userName && password === admin.password) {
password: password, navigate("/admin-menu");
setAuthority: props.setAuthority, } else if (
}); username === pmanager.userName &&
password === pmanager.password
) {
navigate("/PM-project-page");
} else if (username === user.userName && password === user.password) {
navigate("/your-projects");
} }
}
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return ( return (
<> <>
<BackgroundAnimation /> <PreloadBackgroundAnimation />
<div <div
className="flex flex-col h-screen items-center justify-center bg-cover bg-fixed" className="flex flex-col h-screen items-center justify-center bg-cover bg-fixed"
style={{ style={{
@ -47,13 +88,30 @@ function LoginPage(props: {
{" "} {" "}
Please log in to continue{" "} Please log in to continue{" "}
</h2> </h2>
<LoginField <form className="flex flex-col items-center" onSubmit={handleSubmit}>
handleSubmit={handleSubmit} <input
setUsername={setUsername} className="border-2 border-black mb-3 rounded-lg w-[20vw] p-1"
setPassword={setPassword} type="text"
username={username} placeholder="Username"
password={password} onChange={(e) => {
setUsername(e.target.value);
}}
/> />
<input
className="border-2 border-black mb-3 rounded-lg w-[20vw] p-1"
type="password"
placeholder="Password"
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<Button
text="Login"
onClick={(): void => {
return;
}}
/>
</form>
</div> </div>
</div> </div>
</> </>

View file

@ -12,12 +12,11 @@ function ChangeRole(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default ChangeRole; export default ChangeRole;

View file

@ -10,6 +10,6 @@ function PMOtherUsersTR(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMOtherUsersTR; export default PMOtherUsersTR;

View file

@ -14,7 +14,6 @@ function PMProjectMembers(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type={"button"}
/> />
</Link> </Link>
<Link to="/PM-time-role"> <Link to="/PM-time-role">
@ -23,13 +22,12 @@ function PMProjectMembers(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type={"button"}
/> />
</Link> </Link>
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMProjectMembers; export default PMProjectMembers;

View file

@ -31,6 +31,6 @@ function PMProjectPage(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={undefined} />; return <BasicWindow username="Admin" content={content} buttons={undefined} />;
} }
export default PMProjectPage; export default PMProjectPage;

View file

@ -1,6 +1,6 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import TimeReport from "../../Components/NewWeeklyReport"; import TimeReport from "../../Components/TimeReport";
import BackButton from "../../Components/BackButton";
function PMTotalTimeActivity(): JSX.Element { function PMTotalTimeActivity(): JSX.Element {
const content = ( const content = (
@ -18,6 +18,6 @@ function PMTotalTimeActivity(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMTotalTimeActivity; export default PMTotalTimeActivity;

View file

@ -10,6 +10,6 @@ function PMTotalTimeRole(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMTotalTimeRole; export default PMTotalTimeRole;

View file

@ -10,6 +10,6 @@ function PMUnsignedReports(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMUnsignedReports; export default PMUnsignedReports;

View file

@ -1,7 +1,7 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import TimeReport from "../../Components/NewWeeklyReport"; import TimeReport from "../../Components/TimeReport";
import BackButton from "../../Components/BackButton";
function PMViewUnsignedReport(): JSX.Element { function PMViewUnsignedReport(): JSX.Element {
const content = ( const content = (
@ -20,19 +20,17 @@ function PMViewUnsignedReport(): JSX.Element {
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<Button <Button
text="Save" text="Save"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMViewUnsignedReport; export default PMViewUnsignedReport;

View file

@ -1,21 +1,28 @@
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import NewTimeReport from "../../Components/TimeReport";
import BackButton from "../../Components/BackButton"; import BackButton from "../../Components/BackButton";
import EditWeeklyReport from "../../Components/EditWeeklyReport";
function UserEditTimeReportPage(): JSX.Element { function UserEditTimeReportPage(): JSX.Element {
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Edit Time Report</h1> <h1 className="font-bold text-[30px] mb-[20px]">Edit Time Report</h1>
<EditWeeklyReport /> <NewTimeReport />
</> </>
); );
const buttons = ( const buttons = (
<> <>
<Button
text="Save"
onClick={(): void => {
return;
}}
/>
<BackButton /> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default UserEditTimeReportPage; export default UserEditTimeReportPage;

View file

@ -1,30 +1,28 @@
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import NewWeeklyReport from "../../Components/NewWeeklyReport"; import NewTimeReport from "../../Components/TimeReport";
import { Link } from "react-router-dom"; import BackButton from "../../Components/BackButton";
function UserNewTimeReportPage(): JSX.Element { function UserNewTimeReportPage(): JSX.Element {
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">New Time Report</h1> <h1 className="font-bold text-[30px] mb-[20px]">New Time Report</h1>
<NewWeeklyReport /> <NewTimeReport />
</> </>
); );
const buttons = ( const buttons = (
<> <>
<Link to="/project">
<Button <Button
text="Back" text="Submit"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button"
/> />
</Link> <BackButton />
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default UserNewTimeReportPage; export default UserNewTimeReportPage;

View file

@ -27,6 +27,6 @@ function UserProjectPage(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default UserProjectPage; export default UserProjectPage;

View file

@ -15,6 +15,6 @@ function UserViewTimeReportsPage(): JSX.Element {
</> </>
); );
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default UserViewTimeReportsPage; export default UserViewTimeReportsPage;

View file

@ -1,59 +1,31 @@
import React, { useState, createContext, useEffect } from "react";
import { Project } from "../Types/goTypes";
import { api } from "../API/API";
import { Link } from "react-router-dom";
import BasicWindow from "../Components/BasicWindow"; import BasicWindow from "../Components/BasicWindow";
import { ProjectListUser } from "../Components/ProjectListUser";
import { Project } from "../Types/Project";
export const ProjectNameContext = createContext(""); function YourProjectsPage(): JSX.Element {
//TODO: Change so that it reads projects from database
function UserProjectPage(): JSX.Element { const projects: Project[] = [];
const [projects, setProjects] = useState<Project[]>([]); for (let i = 1; i <= 20; i++) {
const [selectedProject, setSelectedProject] = useState(""); projects.push({
id: i,
const getProjects = async (): Promise<void> => { name: "Example Project " + i,
const username = localStorage.getItem("username") ?? ""; // replace with actual username description: "good",
const token = localStorage.getItem("accessToken") ?? ""; // replace with actual token created: "now",
const response = await api.getUserProjects(username, token); owner: "me",
console.log(response); });
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error(response.message);
} }
};
// Call getProjects when the component mounts
useEffect(() => {
getProjects();
}, []);
const handleProjectClick = (projectName: string): void => {
setSelectedProject(projectName);
};
const content = ( const content = (
<ProjectNameContext.Provider value={selectedProject}> <>
<h1 className="font-bold text-[30px] mb-[20px]">Your Projects</h1> <h1 className="font-bold text-[30px] mb-[20px]">Your Projects</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
{projects.map((project, index) => ( <ProjectListUser projects={projects} />
<Link
to={`/project/${project.id}`}
onClick={() => {
handleProjectClick(project.name);
}}
key={index}
>
<h1 className="font-bold underline text-[30px] cursor-pointer">
{project.name}
</h1>
</Link>
))}
</div> </div>
</ProjectNameContext.Provider> </>
); );
const buttons = <></>; const buttons = <></>;
return <BasicWindow username="Admin" content={content} buttons={buttons} />; return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default YourProjectsPage;
export default UserProjectPage;

View file

@ -0,0 +1,13 @@
export interface Project {
id: number;
name: string;
description: string;
owner: string;
created: string; // This is a date
}
export interface NewProject {
name: string;
description: string;
owner: string;
}

View file

@ -0,0 +1,11 @@
// This is how the API responds
export interface User {
id: number;
userName: string;
}
// Used to create a new user
export interface NewUser {
userName: string;
password: string;
}

View file

@ -1,88 +0,0 @@
// Code generated by tygo. DO NOT EDIT.
//////////
// source: WeeklyReport.go
/**
* This is what should be submitted to the server, the username will be derived from the JWT token
*/
export interface NewWeeklyReport {
/**
* The name of the project, as it appears in the database
*/
projectName: string;
/**
* The week number
*/
week: number /* int */;
/**
* Total time spent on development
*/
developmentTime: number /* int */;
/**
* Total time spent in meetings
*/
meetingTime: number /* int */;
/**
* Total time spent on administrative tasks
*/
adminTime: number /* int */;
/**
* Total time spent on personal projects
*/
ownWorkTime: number /* int */;
/**
* Total time spent on studying
*/
studyTime: number /* int */;
/**
* Total time spent on testing
*/
testingTime: number /* int */;
}
//////////
// source: project.go
/**
* Project is a struct that holds the information about a project
*/
export interface Project {
id: number /* int */;
name: string;
description: string;
owner: string;
}
/**
* As it arrives from the client, Owner is derived from the JWT token
*/
export interface NewProject {
name: string;
description: string;
}
//////////
// source: users.go
/**
* User struct represents a user in the system
*/
export interface User {
userId: string;
username: string;
password: string;
}
/**
* Should be used when registering, for example
*/
export interface NewUser {
username: string;
password: string;
}
/**
* PublicUser represents a user that is safe to send over the API (no password)
*/
export interface PublicUser {
userId: string;
username: string;
}

View file

@ -2,9 +2,10 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css"; import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./Pages/App"; import LoginPage from "./Pages/LoginPage.tsx";
import YourProjectsPage from "./Pages/YourProjectsPage.tsx"; import YourProjectsPage from "./Pages/YourProjectsPage.tsx";
import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx"; import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx";
import Register from "./Components/Register.tsx";
import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx"; import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx";
import UserEditTimeReportPage from "./Pages/UserPages/UserEditTimeReportPage.tsx"; import UserEditTimeReportPage from "./Pages/UserPages/UserEditTimeReportPage.tsx";
import UserNewTimeReportPage from "./Pages/UserPages/UserNewTimeReportPage.tsx"; import UserNewTimeReportPage from "./Pages/UserPages/UserNewTimeReportPage.tsx";
@ -34,30 +35,18 @@ import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <App />, element: <LoginPage />,
}, },
{ {
path: "/admin", path: "/your-projects",
element: <AdminMenuPage />,
},
{
path: "/pm",
element: <YourProjectsPage />, element: <YourProjectsPage />,
}, },
{ {
path: "/user", path: "/edit-time-report",
element: <YourProjectsPage />,
},
{
path: "/yourProjects",
element: <YourProjectsPage />,
},
{
path: "/editTimeReport",
element: <UserEditTimeReportPage />, element: <UserEditTimeReportPage />,
}, },
{ {
path: "/newTimeReport", path: "/new-time-report",
element: <UserNewTimeReportPage />, element: <UserNewTimeReportPage />,
}, },
{ {
@ -65,89 +54,101 @@ const router = createBrowserRouter([
element: <UserProjectPage />, element: <UserProjectPage />,
}, },
{ {
path: "/projectPage", path: "/register",
element: <Register />,
},
{
path: "/admin-menu",
element: <AdminMenuPage />,
},
{
path: "/project-page",
element: <UserViewTimeReportsPage />, element: <UserViewTimeReportsPage />,
}, },
{ {
path: "/changeRole", path: "/change-role",
element: <PMChangeRole />, element: <PMChangeRole />,
}, },
{ {
path: "/otherUsersTimeReports", path: "/other-users-time-reports",
element: <PMOtherUsersTR />, element: <PMOtherUsersTR />,
}, },
{ {
path: "/projectMembers", path: "/project-members",
element: <PMProjectMembers />, element: <PMProjectMembers />,
}, },
{ {
path: "/PMProjectPage", path: "/PM-project-page",
element: <PMProjectPage />, element: <PMProjectPage />,
}, },
{ {
path: "/PMTimeActivity", path: "/PM-time-activity",
element: <PMTotalTimeActivity />, element: <PMTotalTimeActivity />,
}, },
{ {
path: "/PMTimeRole", path: "/PM-time-role",
element: <PMTotalTimeRole />, element: <PMTotalTimeRole />,
}, },
{ {
path: "/PMUnsignedReports", path: "/PM-unsigned-reports",
element: <PMUnsignedReports />, element: <PMUnsignedReports />,
}, },
{ {
path: "/PMViewUnsignedReport", path: "/PM-view-unsigned-report",
element: <PMViewUnsignedReport />, element: <PMViewUnsignedReport />,
}, },
{ {
path: "/adminChangeUsername", path: "/admin-add-project",
element: <AdminChangeUsername />,
},
{
path: "/adminProjectAddMember",
element: <AdminProjectAddMember />,
},
{
path: "/adminProjectChangeUserRole",
element: <AdminProjectChangeUserRole />,
},
{
path: "/adminProjectManageMembers",
element: <AdminProjectManageMembers />,
},
{
path: "/adminProjectPage",
element: <AdminProjectPage />,
},
{
path: "/adminProjectStatistics",
element: <AdminProjectStatistics />,
},
{
path: "/adminProjectViewMembers",
element: <AdminProjectViewMemberInfo />,
},
{
path: "/addProject",
element: <AdminAddProject />, element: <AdminAddProject />,
}, },
{ {
path: "/adminAddUser", path: "/admin-add-user",
element: <AdminAddUser />, element: <AdminAddUser />,
}, },
{ {
path: "/adminUserInfo", path: "/admin-change-username",
element: <AdminViewUserInfo />, element: <AdminChangeUsername />,
}, },
{ {
path: "/adminManageProject", path: "/admin-manage-projects",
element: <AdminManageProjects />, element: <AdminManageProjects />,
}, },
{ {
path: "/adminManageUser", path: "/admin-manage-users",
element: <AdminManageUsers />, element: <AdminManageUsers />,
}, },
{
path: "/admin-menu",
element: <AdminMenuPage />,
},
{
path: "/admin-project-add-member",
element: <AdminProjectAddMember />,
},
{
path: "/admin-project-change-user-role",
element: <AdminProjectChangeUserRole />,
},
{
path: "/admin-project-manage-members",
element: <AdminProjectManageMembers />,
},
{
path: "/admin-project-page",
element: <AdminProjectPage />,
},
{
path: "/admin-project-statistics",
element: <AdminProjectStatistics />,
},
{
path: "/admin-project-view-members",
element: <AdminProjectViewMemberInfo />,
},
{
path: "/admin-view-user",
element: <AdminViewUserInfo />,
},
]); ]);
// Semi-hacky way to get the root element // Semi-hacky way to get the root element

View file

@ -1,107 +0,0 @@
import requests
import string
import random
def randomString(len=10):
"""Generate a random string of fixed length"""
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(len))
# Defined once per test run
username = randomString()
projectName = randomString()
# The base URL of the API
base_url = "http://localhost:8080"
# Endpoint to test
registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/submitReport"
getWeeklyReportPath = base_url + "/api/getWeeklyReport"
# Posts the username and password to the register endpoint
def register(username: string, password: string):
print("Registering with username: ", username, " and password: ", password)
response = requests.post(
registerPath, json={"username": username, "password": password}
)
print(response.text)
return response
# Posts the username and password to the login endpoint
def login(username: string, password: string):
print("Logging in with username: ", username, " and password: ", password)
response = requests.post(
loginPath, json={"username": username, "password": password}
)
print(response.text)
return response
def test_login():
response = login(username, "always_same")
assert response.status_code == 200, "Login failed"
print("Login successful")
return response.json()["token"]
def test_create_user():
response = register(username, "always_same")
assert response.status_code == 200, "Registration failed"
print("Registration successful")
def test_add_project():
loginResponse = login(username, "always_same")
token = loginResponse.json()["token"]
response = requests.post(
addProjectPath,
json={"name": projectName, "description": "This is a project"},
headers={"Authorization": "Bearer " + token},
)
print(response.text)
assert response.status_code == 200, "Add project failed"
print("Add project successful")
def test_submit_report():
token = login(username, "always_same").json()["token"]
response = requests.post(
submitReportPath,
json={
"projectName": projectName,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
headers={"Authorization": "Bearer " + token},
)
print(response.text)
assert response.status_code == 200, "Submit report failed"
print("Submit report successful")
def test_get_weekly_report():
token = login(username, "always_same").json()["token"]
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName , "week": 1}
)
print(response.text)
if __name__ == "__main__":
test_create_user()
test_login()
test_add_project()
test_submit_report()
test_get_weekly_report()