Merge branch 'frontend' into gruppDM

This commit is contained in:
Davenludd 2024-04-14 15:30:37 +02:00
commit 97f810fce2
38 changed files with 1202 additions and 337 deletions

View file

@ -2,11 +2,12 @@ package database
import ( import (
"embed" "embed"
"encoding/json"
"errors" "errors"
"fmt"
"path/filepath" "path/filepath"
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@ -22,8 +23,6 @@ type Database interface {
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
DeleteProject(name string, username string) error DeleteProject(name string, username string) error
Migrate() error
MigrateSampleData() 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 AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
@ -41,6 +40,7 @@ type Database interface {
SignWeeklyReport(reportId int, projectManagerId int) error SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error) IsSiteAdmin(username string) (bool, error)
IsProjectManager(username string, projectname string) (bool, error) IsProjectManager(username string, projectname string) (bool, error)
ReportStatistics(username string, projectName string) (*types.Statistics, error)
GetProjectTimes(projectName string) (map[string]int, error) GetProjectTimes(projectName string) (map[string]int, error)
UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
RemoveProject(projectname string) error RemoveProject(projectname string) error
@ -52,7 +52,7 @@ type Database interface {
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use // Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct { type Db struct {
*sqlx.DB *sqlx.Tx
} }
type UserProjectMember struct { type UserProjectMember struct {
@ -94,8 +94,19 @@ const removeUserFromProjectQuery = `DELETE FROM user_roles
WHERE user_id = (SELECT id FROM users WHERE username = ?) WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)` AND project_id = (SELECT id FROM projects WHERE name = ?)`
const reportStatistics = `SELECT SUM(development_time) AS total_development_time,
SUM(meeting_time) AS total_meeting_time,
SUM(admin_time) AS total_admin_time,
SUM(own_work_time) AS total_own_work_time,
SUM(study_time) AS total_study_time,
SUM(testing_time) AS total_testing_time
FROM weekly_reports
WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
GROUP BY user_id, project_id`
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) Database { func DbConnect(dbpath string) sqlx.DB {
// Open the database // Open the database
db, err := sqlx.Connect("sqlite", dbpath) db, err := sqlx.Connect("sqlite", dbpath)
if err != nil { if err != nil {
@ -108,7 +119,25 @@ func DbConnect(dbpath string) Database {
panic(err) panic(err)
} }
return &Db{db} return *db
}
func (d *Db) ReportStatistics(username string, projectName string) (*types.Statistics, error) {
var result types.Statistics
err := d.Get(&result, reportStatistics, username, projectName)
if err != nil {
return nil, err
}
serialized, err := json.Marshal(result)
if err != nil {
return nil, err
}
log.Info(string(serialized))
return &result, nil
} }
func (d *Db) CheckUser(username string, password string) bool { func (d *Db) CheckUser(username string, password string) bool {
@ -212,25 +241,15 @@ func (d *Db) GetProjectId(projectname string) (int, error) {
// Creates a new project in the database, associated with a user // Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error { func (d *Db) AddProject(name string, description string, username string) error {
tx := d.MustBegin()
// Insert the project into the database // Insert the project into the database
_, err := tx.Exec(projectInsert, name, description, username) _, err := d.Exec(projectInsert, name, description, username)
if err != nil { if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err return err
} }
// Add creator to project as project manager // Add creator to project as project manager
_, err = tx.Exec(addUserToProject, username, name, "project_manager") _, err = d.Exec(addUserToProject, username, name, "project_manager")
if err != nil { if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err return err
} }
@ -238,16 +257,7 @@ func (d *Db) AddProject(name string, description string, username string) error
} }
func (d *Db) DeleteProject(projectID string, username string) error { func (d *Db) DeleteProject(projectID string, username string) error {
tx := d.MustBegin() _, err := d.Exec(deleteProject, projectID, username)
_, err := tx.Exec(deleteProject, projectID, username)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("error rolling back transaction: %v, delete error: %v", rollbackErr, err)
}
panic(err)
}
return err return err
} }
@ -471,7 +481,7 @@ func (d *Db) IsSiteAdmin(username string) (bool, error) {
// 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 Migrate(db sqlx.DB) 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 {
@ -483,7 +493,7 @@ func (d *Db) Migrate() error {
return nil return nil
} }
tr := d.MustBegin() tr := db.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -569,7 +579,7 @@ func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, d
} }
// MigrateSampleData applies sample data to the database. // MigrateSampleData applies sample data to the database.
func (d *Db) MigrateSampleData() error { func MigrateSampleData(db sqlx.DB) error {
// Insert sample data // Insert sample data
files, err := sampleData.ReadDir("sample_data") files, err := sampleData.ReadDir("sample_data")
if err != nil { if err != nil {
@ -579,7 +589,7 @@ func (d *Db) MigrateSampleData() error {
if len(files) == 0 { if len(files) == 0 {
println("No sample data files found") println("No sample data files found")
} }
tr := d.MustBegin() tr := db.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -616,7 +626,7 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
WHERE projects.name = ? WHERE projects.name = ?
` `
rows, err := d.DB.Query(query, projectName) rows, err := d.Query(query, projectName)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -9,11 +9,13 @@ import (
// setupState initializes a database instance with necessary setup for testing // setupState initializes a database instance with necessary setup for testing
func setupState() (Database, error) { func setupState() (Database, error) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
err := db.Migrate() err := Migrate(db)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return db, nil
db_iface := Db{db.MustBegin()}
return &db_iface, nil
} }
// This is a more advanced setup that includes more data in the database. // This is a more advanced setup that includes more data in the database.
@ -1078,7 +1080,7 @@ func TestDeleteReport(t *testing.T) {
} }
// Remove report // Remove report
err = db.DeleteReport(report.ReportId,) err = db.DeleteReport(report.ReportId)
if err != nil { if err != nil {
t.Error("RemoveReport failed:", err) t.Error("RemoveReport failed:", err)
} }

View file

@ -1,11 +1,28 @@
package database package database
import "github.com/gofiber/fiber/v2" import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx"
)
// Simple middleware that provides a shared database pool as a local key "db" // Simple middleware that provides a transaction as a local key "db"
func DbMiddleware(db *Database) func(c *fiber.Ctx) error { func DbMiddleware(db *sqlx.DB) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
c.Locals("db", db) tx := db.MustBegin()
defer func() {
if err := tx.Commit(); err != nil {
if err = tx.Rollback(); err != nil {
log.Error("Failed to rollback transaction: ", err)
}
return
}
}()
var db_iface Database = &Db{tx}
c.Locals("db", &db_iface)
return c.Next() return c.Next()
} }
} }

View file

@ -1,58 +1,220 @@
INSERT OR IGNORE INTO users(username, password) INSERT OR IGNORE INTO users(username, password)
VALUES ("admin", "123"); VALUES ("admin", "123"),
("user", "123"),
("user2", "123"),
("John", "123"),
("Emma", "123"),
("Michael", "123"),
("Liam", "123"),
("Oliver", "123"),
("Amelia", "123"),
("Benjamin", "123"),
("Mia", "123"),
("Elijah", "123"),
("Charlotte", "123"),
("Henry", "123"),
("Harper", "123"),
("Lucas", "123"),
("Emily", "123"),
("Alexander", "123"),
("Daniel", "123"),
("Ella", "123"),
("Matthew", "123"),
("Madison", "123"),
("Samuel", "123"),
("Avery", "123"),
("Sofia", "123"),
("David", "123"),
("Victoria", "123"),
("Jackson", "123"),
("Abigail", "123"),
("Gabriel", "123"),
("Luna", "123"),
("Wyatt", "123"),
("Chloe", "123"),
("Nora", "123"),
("Joshua", "123"),
("Hazel", "123"),
("Riley", "123"),
("Scarlett", "123"),
("Aria", "123"),
("Carter", "123"),
("Grace", "123"),
("Jayden", "123"),
("Hannah", "123"),
("Zoe", "123"),
("Luke", "123"),
("Sophia", "123"),
("Jack", "123"),
("Isabella", "123"),
("William", "123"),
("Mason", "123"),
("Evelyn", "123"),
("James", "123"),
("Cynthia", "123"),
("Abraham", "123"),
("Ava", "123"),
("Aiden", "123"),
("Natalie", "123"),
("Lily", "123"),
("Olivia", "123"),
("Alexander", "123"),
("Ethan", "123"),
("Mila", "123"),
("Evelyn", "123"),
("Logan", "123"),
("Riley", "123"),
("Grace", "123"),
("Arnold", "123"),
("Connor", "123"),
("Samantha", "123"),
("Emma", "123"),
("Sarah", "123"),
("Nathan", "123"),
("Layla", "123"),
("Ryan", "123"),
("Zoey", "123"),
("Megan", "123"),
("Christian", "123"),
("Eva", "123"),
("Isaac", "123"),
("Michaela", "123"),
("Caroline", "123"),
("Elijah", "123"),
("Elena", "123"),
("Julian", "123"),
("Sophie", "123"),
("Gabriella", "123"),
("Cole", "123"),
("Hannah", "123"),
("Lucy", "123"),
("Katherine", "123"),
("Benjamin", "123"),
("Ella", "123"),
("Evan", "123");
INSERT OR IGNORE INTO users(username, password) INSERT OR IGNORE INTO projects(name, description, owner_user_id)
VALUES ("user", "123"); VALUES ("projecttest1", "Description for projecttest1", 1),
("projecttest2", "Description for projecttest2", 1),
("projecttest3", "Description for projecttest3", 1),
("projecttest4", "Description for projecttest4", 1),
("projecttest5", "Description for projecttest5", 1),
("projecttest6", "Description for projecttest6", 1),
("projecttest7", "Description for projecttest7", 1),
("projecttest8", "Description for projecttest8", 1),
("projecttest9", "Description for projecttest9", 1),
("projecttest10", "Description for projecttest10", 1),
("projecttest11", "Description for projecttest11", 1),
("projecttest12", "Description for projecttest12", 1),
("projecttest13", "Description for projecttest13", 1),
("projecttest14", "Description for projecttest14", 1),
("projecttest15", "Description for projecttest15", 1);
INSERT OR IGNORE INTO users(username, password) INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES ("user2", "123"); VALUES (1,1,"project_manager"),
(1,2,"project_manager"),
(1,3,"project_manager"),
(1,4,"project_manager"),
(1,5,"project_manager"),
(1,6,"project_manager"),
(1,7,"project_manager"),
(1,8,"project_manager"),
(1,9,"project_manager"),
(1,10,"project_manager"),
(1,11,"project_manager"),
(1,12,"project_manager"),
(1,13,"project_manager"),
(1,14,"project_manager"),
(1,15,"project_manager"),
(2,1,"project_manager"),
(2,2,"member"),
(2,3,"member"),
(2,4,"member"),
(2,5,"member"),
(2,6,"member"),
(2,7,"member"),
(2,8,"member"),
(2,9,"member"),
(2,10,"member"),
(2,11,"member"),
(2,12,"member"),
(2,13,"member"),
(2,14,"member"),
(2,15,"member"),
(3,1,"member"),
(3,2,"member"),
(3,3,"member"),
(3,4,"member"),
(3,5,"member"),
(3,6,"member"),
(3,7,"member"),
(3,8,"member"),
(3,9,"member"),
(3,10,"member"),
(3,11,"member"),
(3,12,"member"),
(3,13,"member"),
(3,14,"member"),
(3,15,"member"),
(4,1,"member"),
(4,2,"member"),
(4,3,"member"),
(4,4,"member"),
(4,5,"member"),
(4,6,"member"),
(4,7,"member"),
(4,8,"member"),
(4,9,"member"),
(4,10,"member"),
(4,11,"member"),
(4,12,"member"),
(4,13,"member"),
(4,14,"member"),
(4,15,"member"),
(5,1,"member"),
(5,2,"member"),
(5,3,"member"),
(5,4,"member"),
(5,5,"member"),
(5,6,"member"),
(5,7,"member"),
(5,8,"member"),
(5,9,"member"),
(5,10,"member"),
(5,11,"member"),
(5,12,"member"),
(5,13,"member"),
(5,14,"member"),
(5,15,"member");
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (2, 1, 12, 100, 50, 30, 150, 80, 20, NULL),
(3, 1, 12, 200, 80, 20, 200, 100, 30, NULL),
(3, 1, 14, 150, 70, 40, 180, 90, 25, NULL),
(3, 2, 12, 120, 60, 35, 160, 85, 15, NULL),
(3, 3, 12, 180, 90, 25, 190, 110, 40, NULL),
(2, 1, 13, 130, 70, 40, 170, 95, 35, NULL),
(3, 1, 15, 140, 60, 50, 200, 120, 30, NULL),
(2, 2, 11, 110, 50, 45, 140, 70, 25, NULL),
(3, 3, 14, 170, 80, 30, 180, 100, 35, NULL),
(3, 3, 15, 200, 100, 20, 220, 130, 45, NULL),
(2, 4, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 5, 14, 150, 70, 30, 180, 90, 25, NULL),
(3, 5, 15, 180, 90, 20, 190, 110, 35, NULL),
(2, 6, 11, 100, 50, 35, 130, 60, 20, NULL),
(3, 7, 14, 170, 80, 25, 180, 100, 30, NULL),
(2, 8, 12, 130, 70, 30, 170, 90, 25, NULL),
(2, 8, 13, 150, 80, 20, 180, 110, 35, NULL),
(3, 9, 12, 140, 60, 40, 180, 100, 30, NULL),
(3, 10, 11, 120, 50, 45, 150, 70, 25, NULL),
(2, 11, 13, 110, 60, 35, 140, 80, 30, NULL),
(3, 12, 12, 160, 70, 30, 180, 100, 35, NULL),
(3, 12, 13, 180, 90, 25, 190, 110, 40, NULL),
(3, 12, 14, 200, 100, 20, 220, 130, 45, NULL),
(2, 13, 11, 100, 50, 45, 130, 60, 20, NULL),
(2, 13, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 14, 13, 140, 70, 30, 160, 90, 35, NULL),
(3, 15, 12, 150, 80, 25, 180, 100, 30, NULL),
(3, 15, 13, 170, 90, 20, 190, 110, 35, NULL);
INSERT OR IGNORE INTO site_admin VALUES (1); INSERT OR IGNORE INTO site_admin VALUES (1);
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest","test project", 1);
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest2","test project2", 1);
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest3","test project3", 1);
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,1,"project_manager");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,2,"project_manager");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,3,"project_manager");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (2,1,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,1,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,2,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,3,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (2,1,"project_manager");
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (2, 1, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 1, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 1, 14, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 2, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 3, 12, 20, 10, 5, 30, 15, 10, NULL);

View file

@ -0,0 +1,50 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func GetStatistics(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)
// Extract project name from query parameters
projectName := c.Query("projectName")
log.Info(username, " trying to get statistics for project: ", projectName)
if projectName == "" {
log.Info("Missing project name")
return c.Status(400).SendString("Missing project name")
}
// If the user is not a project manager, they can't view statistics
pm, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !pm {
log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access")
}
// Retrieve statistics for the project from the database
statistics, err := db.GetDb(c).ReportStatistics(username, projectName)
if err != nil {
log.Error("Error getting statistics for project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning statistics")
// Return the retrieved statistics
return c.JSON(statistics)
}

View file

@ -66,6 +66,15 @@ type WeeklyReport struct {
SignedBy *int `json:"signedBy" db:"signed_by"` SignedBy *int `json:"signedBy" db:"signed_by"`
} }
type Statistics struct {
TotalDevelopmentTime int `json:"totalDevelopmentTime" db:"total_development_time"`
TotalMeetingTime int `json:"totalMeetingTime" db:"total_meeting_time"`
TotalAdminTime int `json:"totalAdminTime" db:"total_admin_time"`
TotalOwnWorkTime int `json:"totalOwnWorkTime" db:"total_own_work_time"`
TotalStudyTime int `json:"totalStudyTime" db:"total_study_time"`
TotalTestingTime int `json:"totalTestingTime" db:"total_testing_time"`
}
type UpdateWeeklyReport struct { type UpdateWeeklyReport struct {
// The name of the project, as it appears in the database // The name of the project, as it appears in the database
ProjectName string `json:"projectName"` ProjectName string `json:"projectName"`

View file

@ -59,13 +59,13 @@ func main() {
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database // Migrate the database
if err = db.Migrate(); err != nil { if err = database.Migrate(db); err != nil {
fmt.Println("Error migrating database: ", err) fmt.Println("Error migrating database: ", err)
os.Exit(1) os.Exit(1)
} }
// Migrate sample data, should not be used in production // Migrate sample data, should not be used in production
if err = db.MigrateSampleData(); err != nil { if err = database.MigrateSampleData(db); err != nil {
fmt.Println("Error migrating sample data: ", err) fmt.Println("Error migrating sample data: ", err)
os.Exit(1) os.Exit(1)
} }
@ -126,12 +126,12 @@ func main() {
api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/removeProject/:projectName", projects.RemoveProject)
api.Delete("/project/:projectID", projects.DeleteProject) api.Delete("/project/:projectID", projects.DeleteProject)
// All report related routes // All report related routes
// reportGroup := api.Group("/report") // Not currently in use // reportGroup := api.Group("/report") // Not currently in use
api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Get("/getWeeklyReport", reports.GetWeeklyReport)
api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports)
api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports)
api.Get("/getStatistics", reports.GetStatistics)
api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport)
api.Put("/signReport/:reportId", reports.SignReport) api.Put("/signReport/:reportId", reports.SignReport)
api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport)

View file

@ -11,6 +11,7 @@ import {
NewProject, NewProject,
WeeklyReport, WeeklyReport,
StrNameChange, StrNameChange,
Statistics,
} from "../Types/goTypes"; } from "../Types/goTypes";
/** /**
@ -258,6 +259,17 @@ interface API {
reportId: number, reportId: number,
token: string, token: string,
): Promise<APIResponse<string>>; ): Promise<APIResponse<string>>;
/**
* Retrieves the total time spent on a project for a particular user (the user is determined by the token)
*
* @param {string} projectName The name of the project
* @param {string} token The authentication token
*/
getStatistics(
projectName: string,
token: string,
): Promise<APIResponse<Statistics>>;
} }
/** An instance of the API */ /** An instance of the API */
@ -664,7 +676,11 @@ export const api: API = {
}); });
if (!response.ok) { if (!response.ok) {
return { success: false, message: "Failed to login" }; return {
success: false,
data: `${response.status}`,
message: "Failed to login",
};
} else { } else {
const data = (await response.json()) as { token: string }; // Update the type of 'data' const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token }; return { success: true, data: data.token };
@ -962,4 +978,30 @@ export const api: API = {
return { success: false, message: "Failed to delete report" }; return { success: false, message: "Failed to delete report" };
} }
}, },
async getStatistics(
token: string,
projectName: string,
): Promise<APIResponse<Statistics>> {
try {
const response = await fetch(
`/api/getStatistics/?projectName=${projectName}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to get statistics" };
} else {
const data = (await response.json()) as Statistics;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get statistics" };
}
},
}; };

View file

@ -1,9 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { api } from "../API/API"; import { api } from "../API/API";
import { NewProject } from "../Types/goTypes"; import { NewProject } from "../Types/goTypes";
import InputField from "./InputField";
import Logo from "../assets/Logo.svg"; import Logo from "../assets/Logo.svg";
import Button from "./Button"; import Button from "./Button";
import { useNavigate } from "react-router-dom";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import DescriptionInput from "./Inputs/DescriptionInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
/** /**
* Provides UI for adding a project to the system. * Provides UI for adding a project to the system.
@ -12,11 +16,26 @@ import Button from "./Button";
function AddProject(): JSX.Element { function AddProject(): JSX.Element {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const navigate = useNavigate();
/** /**
* Tries to add a project to the system * Tries to add a project to the system
*/ */
const handleCreateProject = async (): Promise<void> => { const handleCreateProject = async (): Promise<void> => {
if (
!alphanumeric.test(name) ||
name.length > projNameHighLimit ||
name.length < projNameLowLimit
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (description.length > projNameHighLimit) {
alert("Please provide valid description: \n-Max 100 characters");
return;
}
const project: NewProject = { const project: NewProject = {
name: name.replace(/ /g, ""), name: name.replace(/ /g, ""),
description: description.trim(), description: description.trim(),
@ -30,6 +49,7 @@ function AddProject(): JSX.Element {
alert(`${project.name} added!`); alert(`${project.name} added!`);
setDescription(""); setDescription("");
setName(""); setName("");
navigate("/admin");
} else { } else {
alert("Project not added, name could be taken"); alert("Project not added, name could be taken");
console.error(response.message); console.error(response.message);
@ -44,7 +64,7 @@ function AddProject(): JSX.Element {
<div className="flex flex-col h-fit w-screen items-center justify-center"> <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"> <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 <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 rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void handleCreateProject(); void handleCreateProject();
@ -52,33 +72,29 @@ function AddProject(): JSX.Element {
> >
<img <img
src={Logo} src={Logo}
className="logo w-[7vw] mb-10 mt-10" className="logo w-[7vw] self-center mb-10 mt-10"
alt="TTIME Logo" alt="TTIME Logo"
/> />
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> <h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project Create a new project
</h3> </h3>
<div className="space-y-3"> <ProjectNameInput
<InputField name={name}
label="Name"
type="text"
value={name}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setName(e.target.value); setName(e.target.value);
}} }}
/> />
<InputField <div className="p-2"></div>
label="Description" <DescriptionInput
type="text" desc={description}
value={description}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setDescription(e.target.value); setDescription(e.target.value);
}} }}
placeholder={"Description (Optional)"}
/> />
</div> <div className="flex self-center mt-4 justify-between">
<div className="flex items-center justify-between">
<Button <Button
text="Create" text="Create"
onClick={(): void => { onClick={(): void => {

View file

@ -1,9 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Button from "./Button"; import Button from "./Button";
import AddMember, { AddMemberInfo } from "./AddMember"; import AddMember, { AddMemberInfo } from "./AddMember";
import BackButton from "./BackButton";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import GetAllUsers from "./GetAllUsers"; import GetAllUsers from "./GetAllUsers";
import InputField from "./InputField";
/** /**
* Provides UI for adding a member to a project. * Provides UI for adding a member to a project.
@ -13,6 +13,7 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
const [names, setNames] = useState<string[]>([]); const [names, setNames] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]); const [users, setUsers] = useState<string[]>([]);
const [usersProj, setUsersProj] = useState<ProjectMember[]>([]); const [usersProj, setUsersProj] = useState<ProjectMember[]>([]);
const [search, setSearch] = useState("");
// Gets all users and project members for filtering // Gets all users and project members for filtering
GetAllUsers({ setUsersProp: setUsers }); GetAllUsers({ setUsersProp: setUsers });
@ -36,8 +37,10 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
// Attempts to add all of the selected users to the project // Attempts to add all of the selected users to the project
const handleAddClick = async (): Promise<void> => { const handleAddClick = async (): Promise<void> => {
if (names.length === 0) if (names.length === 0) {
alert("You have to choose at least one user to add"); alert("You have to choose at least one user to add");
return;
}
for (const name of names) { for (const name of names) {
const newMember: AddMemberInfo = { const newMember: AddMemberInfo = {
userName: name, userName: name,
@ -60,22 +63,37 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
}; };
return ( return (
<div className="border-4 border-black bg-white flex flex-col items-center pt-10 rounded-3xl content-center pl-20 pr-20 h-[63vh] w-[50] overflow-auto"> <div className="border-4 border-black bg-white flex flex-col items-center py-10 px-20 rounded-3xl content-center overflow-auto">
<h1 className="text-center font-bold text-[36px] pb-10"> <h1 className="text-center font-bold text-[36px] pb-10">
{props.projectName} {props.projectName}
</h1> </h1>
<p className="p-1 text-center font-bold text-[26px]"> <p className="p-1 text-center font-bold text-[26px]">
Choose users to add: Choose users to add:
</p> </p>
<div className="border-2 border-black pl-2 pr-2 pb-2 rounded-xl text-center overflow-auto h-[26vh] w-[26vh]">
<ul className="text-center font-medium space-y-2"> <div>
<InputField
placeholder={"Search users"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="font-medium space-y-2 border-2 border-black mt-2 px-2 pb-2 rounded-2xl text-center overflow-auto h-[26vh] w-[34vh]">
<div></div> <div></div>
{users.map((user) => ( {users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li <li
className={ className={
names.includes(user) names.includes(user)
? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black" ? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 hover:text-slate-100 hover:cursor-pointer" : "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 transition-all hover:text-white hover:cursor-pointer"
} }
key={user} key={user}
value={user} value={user}
@ -99,9 +117,7 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
}} }}
type="button" type="button"
/> />
<BackButton />
</div> </div>
<p className="text-center text-gray-500 text-xs"></p>
</div> </div>
); );
} }

View file

@ -5,6 +5,7 @@ import ChangeRole, { ProjectRoleChange } from "./ChangeRole";
export default function ChangeRoleView(props: { export default function ChangeRoleView(props: {
projectName: string; projectName: string;
username: string; username: string;
currentRole: string;
}): JSX.Element { }): JSX.Element {
const [selectedRole, setSelectedRole] = useState< const [selectedRole, setSelectedRole] = useState<
"project_manager" | "member" | "" "project_manager" | "member" | ""
@ -21,7 +22,12 @@ export default function ChangeRoleView(props: {
}; };
const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
console.log("Cur: " + props.currentRole + " " + "new: " + selectedRole);
event.preventDefault(); event.preventDefault();
if (selectedRole === props.currentRole) {
alert(`Already ${props.currentRole}, nothing changed`);
return;
}
const roleChangeInfo: ProjectRoleChange = { const roleChangeInfo: ProjectRoleChange = {
username: props.username, username: props.username,
projectname: props.projectName, projectname: props.projectName,
@ -31,35 +37,32 @@ export default function ChangeRoleView(props: {
}; };
return ( return (
<div className="overflow-auto rounded-lg"> <div className="overflow-auto">
<h1 className="font-bold text-[20px]">Select role:</h1> <h1 className="font-bold text-[20px]">Select role:</h1>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="h-[7vh] self-start text-left font-medium overflow-auto border-2 border-black rounded-lg p-2"> <div className="py-1 px-1 w-full self-start text-left font-medium overflow-auto border-2 border-black rounded-2xl">
<div className="hover:font-bold"> <label className="hover:cursor-pointer hover:font-bold">
<label>
<input <input
type="radio" type="radio"
value="project_manager" value="project_manager"
checked={selectedRole === "project_manager"} checked={selectedRole === "project_manager"}
onChange={handleRoleChange} onChange={handleRoleChange}
className="ml-2 mr-2 mb-3" className="m-2"
/> />
Project manager Project manager
</label> </label>
</div> <br />
<div className="hover:font-bold"> <label className="hover:cursor-pointer hover:font-bold">
<label>
<input <input
type="radio" type="radio"
value="member" value="member"
checked={selectedRole === "member"} checked={selectedRole === "member"}
onChange={handleRoleChange} onChange={handleRoleChange}
className="ml-2 mr-2" className="m-2 hover:cursor-pointer"
/> />
Member Member
</label> </label>
</div> </div>
</div>
<Button <Button
text="Change" text="Change"
onClick={(): void => { onClick={(): void => {

View file

@ -9,6 +9,10 @@ function ChangeUsername(props: { nameChange: StrNameChange }): void {
alert("You have to give a new name\n\nName not changed"); alert("You have to give a new name\n\nName not changed");
return; return;
} }
if (props.nameChange.prevName === localStorage.getItem("username")) {
alert("You cannot change admin name");
return;
}
api api
.changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "") .changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => { .then((response: APIResponse<void>) => {

View file

@ -4,19 +4,21 @@
* @returns {JSX.Element} The input field * @returns {JSX.Element} The input field
* @example * @example
* <InputField * <InputField
* type="text"
* label="Example" * label="Example"
* placeholder="New placeholder"
* type="text"
* value={example}
* onChange={(e) => { * onChange={(e) => {
* setExample(e.target.value); * setExample(e.target.value);
* }} * }}
* value={example}
* /> * />
*/ */
function InputField(props: { function InputField(props: {
label: string; label?: string;
type: string; placeholder?: string;
value: string; type?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className=""> <div className="">
@ -30,7 +32,7 @@ function InputField(props: {
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" 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} id={props.label}
type={props.type} type={props.type}
placeholder={props.label} placeholder={props.placeholder}
value={props.value} value={props.value}
onChange={props.onChange} onChange={props.onChange}
/> />

View file

@ -0,0 +1,38 @@
import { projDescHighLimit, projDescLowLimit } from "../../Data/constants";
export default function DescriptionInput(props: {
desc: string;
placeholder: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<>
<input
className={
props.desc.length <= 100
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="true"
id="New desc"
type="text"
placeholder={props.placeholder}
value={props.desc}
onChange={props.onChange}
/>
<div className="my-1">
{props.desc.length > projDescHighLimit && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Description must be under 100 characters
</p>
)}
{props.desc.length <= projDescHighLimit &&
props.desc.length > projDescLowLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project description!
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,44 @@
import { passwordLength } from "../../Data/constants";
import { lowercase } from "../../Data/regex";
export default function PasswordInput(props: {
password: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const password = props.password;
return (
<>
<input
className={
password.length === passwordLength && lowercase.test(password)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New password"
type="password"
placeholder="Password"
value={password}
onChange={props.onChange}
/>
<div className="my-1">
{password.length === passwordLength &&
lowercase.test(props.password) && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid password!
</p>
)}
{password.length !== passwordLength && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Password must be 6 characters
</p>
)}
{!lowercase.test(password) && password !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No number, uppercase or special <br /> characters allowed
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,48 @@
import { projNameHighLimit, projNameLowLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function ProjectNameInput(props: {
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const name = props.name;
return (
<>
<input
className={
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit &&
alphanumeric.test(name)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New name"
type="text"
placeholder="Project name"
value={name}
onChange={props.onChange}
/>
<div className="my-1">
{!alphanumeric.test(name) && name !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{(name.length < projNameLowLimit ||
name.length > projNameHighLimit) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Project name must be 10-99 characters
</p>
)}
{alphanumeric.test(props.name) &&
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project name!
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,50 @@
import { usernameLowLimit, usernameUpLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function UsernameInput(props: {
username: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const username = props.username;
return (
<>
<input
className={
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit &&
alphanumeric.test(props.username)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New username"
type="text"
placeholder="Username"
value={username}
onChange={props.onChange}
/>
<div className="my-1">
{alphanumeric.test(username) &&
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid username!
</p>
)}
{!alphanumeric.test(username) && username !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{!(
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit
) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Username must be 5-10 characters
</p>
)}
</div>
</>
);
}

View file

@ -11,6 +11,10 @@ function LoginCheck(props: {
password: string; password: string;
setAuthority: Dispatch<SetStateAction<number>>; setAuthority: Dispatch<SetStateAction<number>>;
}): void { }): void {
if (props.username === "" || props.password === "") {
alert("Please enter username and password to login");
return;
}
const user: NewUser = { const user: NewUser = {
username: props.username, username: props.username,
password: props.password, password: props.password,
@ -42,7 +46,15 @@ function LoginCheck(props: {
console.error("Token was undefined"); console.error("Token was undefined");
} }
} else { } else {
console.error("Token could not be fetched/No such user"); if (response.data === "500") {
console.error(response.message);
alert("No connection/Error");
} else {
console.error(
"Token could not be fetched/No such user" + response.message,
);
alert("Incorrect login information");
}
} }
}) })
.catch((error) => { .catch((error) => {

View file

@ -33,6 +33,7 @@ function Login(props: {
props.setUsername(e.target.value); props.setUsername(e.target.value);
}} }}
value={props.username} value={props.username}
placeholder={"Username"}
/> />
<InputField <InputField
type="password" type="password"
@ -41,6 +42,7 @@ function Login(props: {
props.setPassword(e.target.value); props.setPassword(e.target.value);
}} }}
value={props.password} value={props.password}
placeholder={"Password"}
/> />
</div> </div>
<Button <Button

View file

@ -1,12 +1,13 @@
import Button from "./Button"; import Button from "./Button";
import UserProjectListAdmin from "./UserProjectListAdmin"; import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react"; import { useState } from "react";
import ChangeRoleView from "./ChangeRoleView";
import RemoveUserFromProj from "./RemoveUserFromProj"; import RemoveUserFromProj from "./RemoveUserFromProj";
import ChangeRoleInput from "./ChangeRoleView";
function MemberInfoModal(props: { function MemberInfoModal(props: {
projectName: string; projectName: string;
username: string; username: string;
role: string;
onClose: () => void; onClose: () => void;
}): JSX.Element { }): JSX.Element {
const [showRoles, setShowRoles] = useState(false); const [showRoles, setShowRoles] = useState(false);
@ -20,22 +21,24 @@ function MemberInfoModal(props: {
}; };
return ( return (
<div <div
className="fixed inset-10 bg-opacity-30 backdrop-blur-sm className="fixed inset-0 bg-opacity-30 backdrop-blur-sm
flex justify-center items-center" flex justify-center items-center"
> >
<div className="border-4 border-black bg-white rounded-lg text-center flex flex-col"> <div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10"> <div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p> <p className="font-bold text-[30px]">{props.username}</p>
<p className="font-bold text-[20px]">{props.role}</p>
<p <p
className="hover:font-bold hover:cursor-pointer underline" className="hover:font-bold hover:cursor-pointer underline mb-2 mt-1"
onClick={handleChangeRole} onClick={handleChangeRole}
> >
(Change Role) (Change Role)
</p> </p>
{showRoles && ( {showRoles && (
<ChangeRoleView <ChangeRoleInput
projectName={props.projectName} projectName={props.projectName}
username={props.username} username={props.username}
currentRole={props.role}
/> />
)} )}
<h2 className="font-bold text-[20px]">Member of these projects:</h2> <h2 className="font-bold text-[20px]">Member of these projects:</h2>

View file

@ -0,0 +1,24 @@
import { useNavigate } from "react-router-dom";
/**
* Renders a navigation button component for navigating to
* different page
* @returns The JSX element representing the navigation button.
*/
export default function NavButton(props: {
navTo: string;
label: string;
}): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(props.navTo);
};
return (
<button
onClick={goBack}
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;"
>
{props.label}
</button>
);
}

View file

@ -4,19 +4,60 @@ import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import GetProjectTimes, { projectTimes } from "./GetProjectTimes"; import GetProjectTimes, { projectTimes } from "./GetProjectTimes";
import DeleteProject from "./DeleteProject"; import DeleteProject from "./DeleteProject";
import InputField from "./InputField";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
function ProjectInfoModal(props: { function ProjectInfoModal(props: {
projectname: string; projectname: string;
onClose: () => void; onClose: () => void;
onClick: (username: string) => void; onClick: (username: string, userRole: string) => void;
}): JSX.Element { }): JSX.Element {
const [showInput, setShowInput] = useState(false);
const [users, setUsers] = useState<ProjectMember[]>([]); const [users, setUsers] = useState<ProjectMember[]>([]);
const [times, setTimes] = useState<projectTimes>(); const [times, setTimes] = useState<projectTimes>();
const [search, setSearch] = useState("");
const [newProjName, setNewProjName] = useState("");
const totalTime = useRef(0); const totalTime = useRef(0);
GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname }); GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname });
const handleChangeNameView = (): void => {
if (showInput) {
setNewProjName("");
setShowInput(false);
} else {
setShowInput(true);
}
};
const handleClickChangeName = (): void => {
if (
newProjName.length > projNameHighLimit ||
newProjName.length < projNameLowLimit ||
!alphanumeric.test(newProjName)
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(
`Are you sure you want to change name of ${props.projectname} to ${newProjName}?`,
)
) {
//TODO: change and insert change name functionality
alert("Not implemented yet");
setNewProjName("");
} else {
alert("Name was not changed!");
}
};
useEffect(() => { useEffect(() => {
if (times?.totalTime !== undefined) { if (times?.totalTime !== undefined) {
totalTime.current = times.totalTime; totalTime.current = times.totalTime;
@ -28,35 +69,79 @@ function ProjectInfoModal(props: {
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center" flex justify-center items-center"
> >
<div className="border-4 border-black bg-white p-2 rounded-2xl text-center h-[61vh] w-[40] overflow-auto"> <div className="border-4 border-black bg-white p-2 rounded-2xl text-center h-[64vh] w-[40] overflow-auto">
<div className="pl-10 pr-10"> <div className="pl-10 pr-10">
<h1 className="font-bold text-[32px] mb-[20px]"> <h1 className="font-bold text-[32px]">{props.projectname}</h1>
{props.projectname} <p
</h1> className="mb-4 hover:font-bold hover:cursor-pointer hover:underline"
<div className="p-1 text-center"> onClick={handleChangeNameView}
<h2 className="text-[20px] font-bold">Statistics:</h2> >
(Change project name)
</p>
{showInput && (
<>
<h2 className="text-[20px] font-bold pb-2">Change name:</h2>
<div className="border-2 rounded-2xl border-black px-6 pt-6 pb-1 mb-7">
<ProjectNameInput
name={newProjName}
onChange={function (e): void {
setNewProjName(e.target.value);
}}
/>
<div className="px-6 grid grid-cols-2 gap-10">
<Button
text={"Change"}
onClick={function (): void {
handleClickChangeName();
}}
type={"submit"}
/>
<Button
text={"Close"}
onClick={function (): void {
handleChangeNameView();
}}
type={"submit"}
/>
</div> </div>
<div className="border-2 border-black rounded-lg h-[8vh] text-left divide-y-2 flex flex-col overflow-auto mx-10"> </div>
<p className="p-2">Number of members: {users.length}</p> </>
<p className="p-2"> )}
<h2 className="text-[20px] font-bold pb-2">Statistics:</h2>
<div className="border-2 border-black rounded-2xl px-2 py-1 text-left divide-y-2 flex flex-col overflow-auto">
<p>Number of members: {users.length}</p>
<p>
Total time reported:{" "} Total time reported:{" "}
{Math.floor(totalTime.current / 60 / 24) + " d "} {Math.floor(totalTime.current / 60 / 24) + " d "}
{Math.floor((totalTime.current / 60) % 24) + " h "} {Math.floor((totalTime.current / 60) % 24) + " h "}
{(totalTime.current % 60) + " m "} {(totalTime.current % 60) + " m "}
</p> </p>
</div> </div>
<div className="h-[6vh] p-7 text-center"> <h3 className="pt-7 text-[20px] font-bold">Project members:</h3>
<h2 className="text-[20px] font-bold">Project members:</h2> <div className="">
</div> <InputField
<div className="border-2 border-black p-2 rounded-lg text-center overflow-auto h-[24vh] mx-10"> placeholder={"Search member"}
<ul className="text-left font-medium space-y-2"> type={"Text"}
<div></div> value={search}
{users.map((user) => ( onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="border-2 border-black mt-2 p-2 rounded-2xl text-left overflow-auto h-[24vh] font-medium space-y-2">
{users
.filter((user) => {
return search.toLowerCase() === ""
? user.Username
: user.Username.toLowerCase().includes(
search.toLowerCase(),
);
})
.map((user) => (
<li <li
className="items-start p-1 border-2 border-black rounded-lg bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer" className="items-start px-2 py-1 border-2 border-black rounded-2xl bg-orange-200 transition-all hover:bg-orange-600 hover:text-white hover:cursor-pointer"
key={user.Username} key={user.Username}
onClick={() => { onClick={() => {
props.onClick(user.Username); props.onClick(user.Username, user.UserRole);
}} }}
> >
<span> <span>

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { NewProject } from "../Types/goTypes"; import { NewProject } from "../Types/goTypes";
import ProjectInfoModal from "./ProjectInfoModal"; import ProjectInfoModal from "./ProjectInfoModal";
import MemberInfoModal from "./MemberInfoModal"; import MemberInfoModal from "./MemberInfoModal";
import InputField from "./InputField";
/** /**
* A list of projects for admin manage projects page, that sets an onClick * A list of projects for admin manage projects page, that sets an onClick
@ -21,9 +22,12 @@ export function ProjectListAdmin(props: {
const [projectName, setProjectName] = useState(""); const [projectName, setProjectName] = useState("");
const [userModalVisible, setUserModalVisible] = useState(false); const [userModalVisible, setUserModalVisible] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [userRole, setUserRole] = useState("");
const [search, setSearch] = useState("");
const handleClickUser = (username: string): void => { const handleClickUser = (username: string, userRole: string): void => {
setUsername(username); setUsername(username);
setUserRole(userRole);
setUserModalVisible(true); setUserModalVisible(true);
}; };
@ -39,11 +43,13 @@ export function ProjectListAdmin(props: {
const handleCloseUser = (): void => { const handleCloseUser = (): void => {
setUsername(""); setUsername("");
setUserRole("");
setUserModalVisible(false); setUserModalVisible(false);
}; };
return ( return (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1>
{projectModalVisible && ( {projectModalVisible && (
<ProjectInfoModal <ProjectInfoModal
onClose={handleCloseProject} onClose={handleCloseProject}
@ -56,13 +62,28 @@ export function ProjectListAdmin(props: {
onClose={handleCloseUser} onClose={handleCloseUser}
username={username} username={username}
projectName={projectName} projectName={projectName}
role={userRole}
/> />
)} )}
<div> <div>
<ul className="font-bold underline text-[30px] cursor-pointer padding"> <InputField
{props.projects.map((project) => ( placeholder={"Search"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="mt-3 border-2 text-left border-black rounded-2xl px-2 divide-y divide-gray-300 font-semibold text-[30px] cursor-pointer overflow-auto h-[60vh] w-[40vw]">
{props.projects
.filter((project) => {
return search.toLowerCase() === ""
? project.name
: project.name.toLowerCase().includes(search.toLowerCase());
})
.map((project) => (
<li <li
className="pt-5" className="hover:font-extrabold hover:underline p-1"
key={project.name} key={project.name}
onClick={() => { onClick={() => {
handleClickProject(project.name); handleClickProject(project.name);

View file

@ -3,25 +3,44 @@ import { NewUser } from "../Types/goTypes";
import { api } from "../API/API"; import { api } from "../API/API";
import Logo from "../assets/Logo.svg"; import Logo from "../assets/Logo.svg";
import Button from "./Button"; import Button from "./Button";
import InputField from "./InputField"; import UsernameInput from "./Inputs/UsernameInput";
import PasswordInput from "./Inputs/PasswordInput";
import { alphanumeric, lowercase } from "../Data/regex";
import {
passwordLength,
usernameLowLimit,
usernameUpLimit,
} from "../Data/constants";
/** /**
* Renders a registration form for the admin to add new users in. * Renders a registration form for the admin to add new users in.
* @returns The JSX element representing the registration form. * @returns The JSX element representing the registration form.
*/ */
export default function Register(): JSX.Element { export default function Register(): JSX.Element {
const [username, setUsername] = useState<string>(); const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>(); const [password, setPassword] = useState<string>("");
const [errMessage, setErrMessage] = useState<string>(); const [errMessage, setErrMessage] = useState<string>("");
const handleRegister = async (): Promise<void> => { const handleRegister = async (): Promise<void> => {
if (username === "" || password === "") { if (
alert("Must provide username and password"); username.length > usernameUpLimit ||
username.length < usernameLowLimit ||
!alphanumeric.test(username)
) {
alert(
"Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)",
);
return;
}
if (password.length !== passwordLength || !lowercase.test(password)) {
alert(
"Please provide valid password: \n-Exactly 6 characters \n-No uppercase letters \n-No numbers \n-No special characters (.-!?/*)",
);
return; return;
} }
const newUser: NewUser = { const newUser: NewUser = {
username: username?.replace(/ /g, "") ?? "", username: username,
password: password ?? "", password: password,
}; };
const response = await api.registerUser(newUser); const response = await api.registerUser(newUser);
if (response.success) { if (response.success) {
@ -39,7 +58,7 @@ export default function Register(): JSX.Element {
<div className="flex flex-col h-fit w-screen items-center justify-center"> <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"> <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 <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 rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void handleRegister(); void handleRegister();
@ -47,31 +66,28 @@ export default function Register(): JSX.Element {
> >
<img <img
src={Logo} src={Logo}
className="logo w-[7vw] mb-10 mt-10" className="logo self-center w-[7vw] mb-10 mt-10"
alt="TTIME Logo" alt="TTIME Logo"
/> />
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> <h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Register New User Register New User
</h3> </h3>
<div className="space-y-3">
<InputField <UsernameInput
label="Username" username={username}
type="text"
value={username ?? ""}
onChange={(e) => { onChange={(e) => {
setUsername(e.target.value); setUsername(e.target.value);
}} }}
/> />
<InputField <div className="py-2" />
label="Password" <PasswordInput
type="password" password={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 self-center justify-between">
<Button <Button
text="Register" text="Register"
onClick={(): void => { onClick={(): void => {

View file

@ -2,35 +2,104 @@ import Button from "./Button";
import DeleteUser from "./DeleteUser"; import DeleteUser from "./DeleteUser";
import UserProjectListAdmin from "./UserProjectListAdmin"; import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react"; import { useState } from "react";
import InputField from "./InputField";
import ChangeUsername from "./ChangeUsername"; import ChangeUsername from "./ChangeUsername";
import { StrNameChange } from "../Types/goTypes"; import { StrNameChange } from "../Types/goTypes";
import UsernameInput from "./Inputs/UsernameInput";
import PasswordInput from "./Inputs/PasswordInput";
import { alphanumeric, lowercase } from "../Data/regex";
import {
passwordLength,
usernameLowLimit,
usernameUpLimit,
} from "../Data/constants";
function UserInfoModal(props: { function UserInfoModal(props: {
isVisible: boolean; isVisible: boolean;
username: string; username: string;
onClose: () => void; onClose: () => void;
}): JSX.Element { }): JSX.Element {
const [showInput, setShowInput] = useState(false); const [showNameInput, setShowNameInput] = useState(false);
const [showPwordInput, setShowPwordInput] = useState(false);
const [newUsername, setNewUsername] = useState(""); const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
if (!props.isVisible) { if (!props.isVisible) {
return <></>; return <></>;
} }
const handleChangeNameView = (): void => { /*
if (showInput) { * Switches name input between visible/invisible
setShowInput(false); * and makes password input invisible
*/
const handleShowNameInput = (): void => {
if (showPwordInput) setShowPwordInput(false);
if (showNameInput) {
setShowNameInput(false);
setNewUsername("");
} else { } else {
setShowInput(true); setShowNameInput(true);
setNewPassword("");
} }
}; };
/*
* Switches password input between visible/invisible
* and makes username input invisible
*/
const handleShowPwordInput = (): void => {
if (showNameInput) setShowNameInput(false);
if (showPwordInput) {
setShowPwordInput(false);
setNewPassword("");
} else {
setShowPwordInput(true);
setNewUsername("");
}
};
// Handles name change and checks if new name meets requirements
const handleClickChangeName = (): void => { const handleClickChangeName = (): void => {
if (
!alphanumeric.test(newUsername) ||
newUsername.length > usernameUpLimit ||
newUsername.length < usernameLowLimit
) {
alert(
"Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(
`Do you really want to change username of ${props.username} to ${newUsername}?`,
)
) {
const nameChange: StrNameChange = { const nameChange: StrNameChange = {
prevName: props.username, prevName: props.username,
newName: newUsername.replace(/ /g, ""), newName: newUsername.replace(/ /g, ""),
}; };
ChangeUsername({ nameChange: nameChange }); ChangeUsername({ nameChange: nameChange });
} else {
alert("Name was not changed!");
}
};
// Handles password change and checks if new password meets requirements
const handleClickChangePassword = (): void => {
if (newPassword.length !== passwordLength || !lowercase.test(newPassword)) {
alert(
"Please provide valid password: \n-Exactly 6 characters \n-No uppercase letters \n-No numbers \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(`Are you sure you want to change password of ${props.username}?`)
) {
//TODO: insert change password functionality
alert("Not implemented yet");
setNewPassword("");
} else {
alert("Password was not changed!");
}
}; };
return ( return (
@ -38,23 +107,37 @@ function UserInfoModal(props: {
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center" flex justify-center items-center"
> >
<div className="border-4 border-black bg-white rounded-lg text-center flex flex-col"> <div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10"> <div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p> <p className="font-bold text-[30px]">{props.username}</p>
<p <p className="mt-2 font-bold text-[20px]">Change:</p>
className="mb-[10px] hover:font-bold hover:cursor-pointer underline" <p className="mt-2 space-x-3 mb-[10px]">
onClick={handleChangeNameView} <span
className={
showNameInput
? "items-start font-semibold py-1 px-2 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start font-medium py-1 px-2 border-2 border-gray-500 text-white rounded-full bg-orange-300 hover:bg-orange-400 transition-all hover:text-gray-100 hover:border-gray-600 hover:cursor-pointer"
}
onClick={handleShowNameInput}
> >
(Change Username) Username
</span>{" "}
<span
className={
showPwordInput
? "items-start font-semibold py-1 px-2 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start font-medium py-1 px-2 border-2 border-gray-500 text-white rounded-full bg-orange-300 hover:bg-orange-400 transition-all hover:text-gray-100 hover:border-gray-600 hover:cursor-pointer"
}
onClick={handleShowPwordInput}
>
Password
</span>
</p> </p>
{showInput && ( {showNameInput && (
<div> <div className="mt-7">
<InputField <UsernameInput
label={"New username"} username={newUsername}
type={"text"} onChange={(e) => {
value={newUsername}
onChange={function (e): void {
e.defaultPrevented;
setNewUsername(e.target.value); setNewUsername(e.target.value);
}} }}
/> />
@ -67,6 +150,23 @@ function UserInfoModal(props: {
/> />
</div> </div>
)} )}
{showPwordInput && (
<div className="mt-7">
<PasswordInput
password={newPassword}
onChange={(e) => {
setNewPassword(e.target.value);
}}
/>
<Button
text={"Change"}
onClick={function (): void {
handleClickChangePassword();
}}
type={"submit"}
/>
</div>
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2> <h2 className="font-bold text-[20px]">Member of these projects:</h2>
<UserProjectListAdmin username={props.username} /> <UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6"> <div className="items-center space-x-6">
@ -87,7 +187,9 @@ function UserInfoModal(props: {
text={"Close"} text={"Close"}
onClick={function (): void { onClick={function (): void {
setNewUsername(""); setNewUsername("");
setShowInput(false); setNewPassword("");
setShowNameInput(false);
setShowPwordInput(false);
props.onClose(); props.onClose();
}} }}
type="button" type="button"

View file

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import UserInfoModal from "./UserInfoModal"; import UserInfoModal from "./UserInfoModal";
import InputField from "./InputField";
/** /**
* A list of users for admin manage users page, that sets an onClick * A list of users for admin manage users page, that sets an onClick
@ -15,6 +16,7 @@ import UserInfoModal from "./UserInfoModal";
export function UserListAdmin(props: { users: string[] }): JSX.Element { export function UserListAdmin(props: { users: string[] }): JSX.Element {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [search, setSearch] = useState("");
const handleClick = (username: string): void => { const handleClick = (username: string): void => {
setUsername(username); setUsername(username);
@ -28,16 +30,31 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element {
return ( return (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Users</h1>
<UserInfoModal <UserInfoModal
onClose={handleClose} onClose={handleClose}
isVisible={modalVisible} isVisible={modalVisible}
username={username} username={username}
/> />
<div> <div>
<ul className="font-bold underline text-[30px] cursor-pointer padding"> <InputField
{props.users.map((user) => ( placeholder={"Search"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="mt-3 border-2 text-left border-black rounded-2xl px-2 divide-y divide-gray-300 font-semibold text-[30px] transition-all cursor-pointer overflow-auto h-[60vh] w-[40vw]">
{props.users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li <li
className="pt-5" className="hover:font-extrabold hover:underline p-1"
key={user} key={user}
onClick={() => { onClick={() => {
handleClick(user); handleClick(user);

View file

@ -8,7 +8,7 @@ function UserProjectListAdmin(props: { username: string }): JSX.Element {
GetProjects({ setProjectsProp: setProjects, username: props.username }); GetProjects({ setProjectsProp: setProjects, username: props.username });
return ( return (
<div className="border-2 border-black bg-white rounded-lg text-left overflow-auto h-[15vh] font-medium"> <div className="border-2 border-black bg-white rounded-2xl text-left overflow-auto h-[15vh] font-medium">
<ul className="divide-y-2"> <ul className="divide-y-2">
{projects.map((project) => ( {projects.map((project) => (
<li className="mx-2 my-1" key={project.id}> <li className="mx-2 my-1" key={project.id}>

View file

@ -0,0 +1,36 @@
//Different character limits certain strings
/**
* Allowed character length for password
*/
export const passwordLength = 6;
/**
* Lower limit for username length
*/
export const usernameLowLimit = 5;
/**
* Upper limit for password length
*/
export const usernameUpLimit = 10;
/**
* Lower limit for project name length
*/
export const projNameLowLimit = 10;
/**
* Upper limit for project name length
*/
export const projNameHighLimit = 99;
/**
* Upper limit for project description length
*/
export const projDescLowLimit = 0;
/**
* Upper limit for project description length
*/
export const projDescHighLimit = 99;

View file

@ -0,0 +1,9 @@
/**
* Only alphanumerical characters
*/
export const alphanumeric = /^[a-zA-Z0-9]+$/;
/**
* Only lowercase letters
*/
export const lowercase = /^[a-z]+$/;

View file

@ -1,11 +1,11 @@
import { Link } from "react-router-dom"; 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";
import { ProjectListAdmin } from "../../Components/ProjectListAdmin"; import { ProjectListAdmin } from "../../Components/ProjectListAdmin";
import { Project } from "../../Types/goTypes"; import { Project } from "../../Types/goTypes";
import GetProjects from "../../Components/GetProjects"; import GetProjects from "../../Components/GetProjects";
import { useState } from "react"; import { useState } from "react";
import NavButton from "../../Components/NavButton";
function AdminManageProjects(): JSX.Element { function AdminManageProjects(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
@ -13,14 +13,7 @@ function AdminManageProjects(): JSX.Element {
setProjectsProp: setProjects, setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "", username: localStorage.getItem("username") ?? "",
}); });
const content = ( const content = <ProjectListAdmin projects={projects} />;
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</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]">
<ProjectListAdmin projects={projects} />
</div>
</>
);
const buttons = ( const buttons = (
<> <>
@ -33,7 +26,7 @@ function AdminManageProjects(): JSX.Element {
type="button" type="button"
/> />
</Link> </Link>
<BackButton /> <NavButton navTo="/admin" label={"Back"} />
</> </>
); );

View file

@ -12,14 +12,7 @@ function AdminManageUsers(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const content = ( const content = <UserListAdmin users={users} />;
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Users</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]">
<UserListAdmin users={users} />
</div>
</>
);
const buttons = ( const buttons = (
<> <>

View file

@ -5,14 +5,14 @@ function AdminMenuPage(): JSX.Element {
const content = ( const content = (
<> <>
<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-auto space-y-[10vh] p-[30px]">
<Link to="/adminManageUser"> <Link to="/adminManageUser">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Manage Users Manage Users
</h1> </h1>
</Link> </Link>
<Link to="/adminManageProject"> <Link to="/adminManageProject">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Manage Projects Manage Projects
</h1> </h1>
</Link> </Link>

View file

@ -1,11 +1,12 @@
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import AddUserToProject from "../../Components/AddUserToProject"; import AddUserToProject from "../../Components/AddUserToProject";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
function AdminProjectAddMember(): JSX.Element { function AdminProjectAddMember(): JSX.Element {
const projectName = useLocation().search.slice(1); const projectName = useLocation().search.slice(1);
const content = <AddUserToProject projectName={projectName} />; const content = <AddUserToProject projectName={projectName} />;
const buttons = <></>; const buttons = <BackButton />;
return <BasicWindow content={content} buttons={buttons} />; return <BasicWindow content={content} buttons={buttons} />;
} }
export default AdminProjectAddMember; export default AdminProjectAddMember;

View file

@ -1,15 +0,0 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
function AdminProjectStatistics(): JSX.Element {
const content = <></>;
const buttons = (
<>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectStatistics;

View file

@ -124,6 +124,14 @@ export interface WeeklyReport {
*/ */
signedBy?: number /* int */; signedBy?: number /* int */;
} }
export interface Statistics {
totalDevelopmentTime: number /* int */;
totalMeetingTime: number /* int */;
totalAdminTime: number /* int */;
totalOwnWorkTime: number /* int */;
totalStudyTime: number /* int */;
totalTestingTime: number /* int */;
}
export interface UpdateWeeklyReport { export interface UpdateWeeklyReport {
/** /**
* The name of the project, as it appears in the database * The name of the project, as it appears in the database

View file

@ -23,7 +23,6 @@ import AdminManageProjects from "./Pages/AdminPages/AdminManageProjects.tsx";
import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx"; import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx";
import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx"; import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx";
import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx"; import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx";
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import NotFoundPage from "./Pages/NotFoundPage.tsx"; import NotFoundPage from "./Pages/NotFoundPage.tsx";
import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx"; import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx";
import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx"; import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx";
@ -103,10 +102,6 @@ const router = createBrowserRouter([
path: "/adminProjectAddMember", path: "/adminProjectAddMember",
element: <AdminProjectAddMember />, element: <AdminProjectAddMember />,
}, },
{
path: "/adminProjectStatistics",
element: <AdminProjectStatistics />,
},
{ {
path: "/addProject", path: "/addProject",
element: <AdminAddProject />, element: <AdminAddProject />,

View file

@ -36,6 +36,7 @@ removeProjectPath = base_url + "/api/removeProject"
promoteToPmPath = base_url + "/api/promoteToPm" promoteToPmPath = base_url + "/api/promoteToPm"
unsignReportPath = base_url + "/api/unsignReport" unsignReportPath = base_url + "/api/unsignReport"
deleteReportPath = base_url + "/api/deleteReport" deleteReportPath = base_url + "/api/deleteReport"
getStatisticsPath = base_url + "/api/getStatistics"
debug_output = False debug_output = False
@ -162,3 +163,11 @@ def deleteReport(report_id: int):
return requests.delete( return requests.delete(
deleteReportPath + "/" + str(report_id), deleteReportPath + "/" + str(report_id),
) )
def getStatistics(token: string, projectName: string):
response = requests.get(
getStatisticsPath,
headers = {"Authorization": "Bearer " + token},
params={"projectName": projectName}
)
return response.json()

View file

@ -625,6 +625,46 @@ def test_delete_report():
gprint("test_delete_report successful") gprint("test_delete_report successful")
def test_get_statistics():
# Create admin
admin_username = randomString()
admin_password = randomString()
project_name = "project" + randomString()
token = register_and_login(admin_username, admin_password)
response = create_project(token, project_name)
assert response.status_code == 200, "Create project failed"
response = submitReport(token, {
"projectName": project_name,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
})
response = submitReport(token, {
"projectName": project_name,
"week": 2,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
})
assert response.status_code == 200, "Submit report failed"
stats = getStatistics(token, project_name)
assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct"
gprint("test_get_statistics successful")
if __name__ == "__main__": if __name__ == "__main__":
@ -650,3 +690,4 @@ if __name__ == "__main__":
test_change_user_name() test_change_user_name()
test_update_weekly_report() test_update_weekly_report()
test_get_other_users_report_as_pm() test_get_other_users_report_as_pm()
test_get_statistics()