Merge branch 'frontend' into gruppDM
This commit is contained in:
		
						commit
						97f810fce2
					
				
					 38 changed files with 1202 additions and 337 deletions
				
			
		| 
						 | 
					@ -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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -1088,5 +1090,5 @@ func TestDeleteReport(t *testing.T) {
 | 
				
			||||||
	if err == nil {
 | 
						if err == nil {
 | 
				
			||||||
		t.Error("RemoveReport failed: report not removed")
 | 
							t.Error("RemoveReport failed: report not removed")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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),
 | 
				
			||||||
INSERT OR IGNORE INTO users(username, password)
 | 
					        ("projecttest3", "Description for projecttest3", 1),
 | 
				
			||||||
VALUES ("user2", "123");
 | 
					        ("projecttest4", "Description for projecttest4", 1),
 | 
				
			||||||
 | 
					        ("projecttest5", "Description for projecttest5", 1),
 | 
				
			||||||
INSERT OR IGNORE INTO site_admin VALUES (1);
 | 
					        ("projecttest6", "Description for projecttest6", 1),
 | 
				
			||||||
 | 
					        ("projecttest7", "Description for projecttest7", 1),
 | 
				
			||||||
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
 | 
					        ("projecttest8", "Description for projecttest8", 1),
 | 
				
			||||||
VALUES ("projecttest","test project", 1);
 | 
					        ("projecttest9", "Description for projecttest9", 1),
 | 
				
			||||||
 | 
					        ("projecttest10", "Description for projecttest10", 1),
 | 
				
			||||||
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
 | 
					        ("projecttest11", "Description for projecttest11", 1),
 | 
				
			||||||
VALUES ("projecttest2","test project2", 1);
 | 
					        ("projecttest12", "Description for projecttest12", 1),
 | 
				
			||||||
 | 
					        ("projecttest13", "Description for projecttest13", 1),
 | 
				
			||||||
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
 | 
					        ("projecttest14", "Description for projecttest14", 1),
 | 
				
			||||||
VALUES ("projecttest3","test project3", 1);
 | 
					        ("projecttest15", "Description for projecttest15", 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
				
			||||||
VALUES (1,1,"project_manager");
 | 
					VALUES  (1,1,"project_manager"),
 | 
				
			||||||
 | 
					        (1,2,"project_manager"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (1,3,"project_manager"),
 | 
				
			||||||
VALUES (1,2,"project_manager");
 | 
					        (1,4,"project_manager"),
 | 
				
			||||||
 | 
					        (1,5,"project_manager"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (1,6,"project_manager"),
 | 
				
			||||||
VALUES (1,3,"project_manager");
 | 
					        (1,7,"project_manager"),
 | 
				
			||||||
 | 
					        (1,8,"project_manager"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (1,9,"project_manager"),
 | 
				
			||||||
VALUES (2,1,"member");
 | 
					        (1,10,"project_manager"),
 | 
				
			||||||
 | 
					        (1,11,"project_manager"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (1,12,"project_manager"),
 | 
				
			||||||
VALUES (3,1,"member");
 | 
					        (1,13,"project_manager"),
 | 
				
			||||||
 | 
					        (1,14,"project_manager"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (1,15,"project_manager"),
 | 
				
			||||||
VALUES (3,2,"member");
 | 
					        (2,1,"project_manager"),
 | 
				
			||||||
 | 
					        (2,2,"member"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (2,3,"member"),
 | 
				
			||||||
VALUES (3,3,"member");
 | 
					        (2,4,"member"),
 | 
				
			||||||
 | 
					        (2,5,"member"),
 | 
				
			||||||
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
 | 
					        (2,6,"member"),
 | 
				
			||||||
VALUES (2,1,"project_manager");
 | 
					        (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)
 | 
					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);
 | 
					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 weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
 | 
					INSERT OR IGNORE INTO site_admin VALUES (1);
 | 
				
			||||||
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);
 | 
					 | 
				
			||||||
							
								
								
									
										50
									
								
								backend/internal/handlers/reports/Statistics.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								backend/internal/handlers/reports/Statistics.go
									
										
									
									
									
										Normal 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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"`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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" };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"
 | 
					            onChange={(e) => {
 | 
				
			||||||
              type="text"
 | 
					              e.preventDefault();
 | 
				
			||||||
              value={name}
 | 
					              setName(e.target.value);
 | 
				
			||||||
              onChange={(e) => {
 | 
					            }}
 | 
				
			||||||
                e.preventDefault();
 | 
					          />
 | 
				
			||||||
                setName(e.target.value);
 | 
					          <div className="p-2"></div>
 | 
				
			||||||
              }}
 | 
					          <DescriptionInput
 | 
				
			||||||
            />
 | 
					            desc={description}
 | 
				
			||||||
            <InputField
 | 
					            onChange={(e) => {
 | 
				
			||||||
              label="Description"
 | 
					              e.preventDefault();
 | 
				
			||||||
              type="text"
 | 
					              setDescription(e.target.value);
 | 
				
			||||||
              value={description}
 | 
					            }}
 | 
				
			||||||
              onChange={(e) => {
 | 
					            placeholder={"Description (Optional)"}
 | 
				
			||||||
                e.preventDefault();
 | 
					          />
 | 
				
			||||||
                setDescription(e.target.value);
 | 
					          <div className="flex self-center mt-4 justify-between">
 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div className="flex items-center justify-between">
 | 
					 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
              text="Create"
 | 
					              text="Create"
 | 
				
			||||||
              onClick={(): void => {
 | 
					              onClick={(): void => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,32 +63,47 @@ 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
 | 
				
			||||||
            <li
 | 
					            .filter((user) => {
 | 
				
			||||||
              className={
 | 
					              return search.toLowerCase() === ""
 | 
				
			||||||
                names.includes(user)
 | 
					                ? 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"
 | 
					                : user.toLowerCase().includes(search.toLowerCase());
 | 
				
			||||||
                  : "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 hover:text-slate-100 hover:cursor-pointer"
 | 
					            })
 | 
				
			||||||
              }
 | 
					            .map((user) => (
 | 
				
			||||||
              key={user}
 | 
					              <li
 | 
				
			||||||
              value={user}
 | 
					                className={
 | 
				
			||||||
              onClick={() => {
 | 
					                  names.includes(user)
 | 
				
			||||||
                handleUserClick(user);
 | 
					                    ? "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 transition-all hover:text-white hover:cursor-pointer"
 | 
				
			||||||
            >
 | 
					                }
 | 
				
			||||||
              <span>{user}</span>
 | 
					                key={user}
 | 
				
			||||||
            </li>
 | 
					                value={user}
 | 
				
			||||||
          ))}
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  handleUserClick(user);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <span>{user}</span>
 | 
				
			||||||
 | 
					              </li>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
 | 
					      <p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,34 +37,31 @@ 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="m-2"
 | 
				
			||||||
                className="ml-2 mr-2 mb-3"
 | 
					            />
 | 
				
			||||||
              />
 | 
					            Project manager
 | 
				
			||||||
              Project manager
 | 
					          </label>
 | 
				
			||||||
            </label>
 | 
					          <br />
 | 
				
			||||||
          </div>
 | 
					          <label className="hover:cursor-pointer hover:font-bold">
 | 
				
			||||||
          <div className="hover:font-bold">
 | 
					            <input
 | 
				
			||||||
            <label>
 | 
					              type="radio"
 | 
				
			||||||
              <input
 | 
					              value="member"
 | 
				
			||||||
                type="radio"
 | 
					              checked={selectedRole === "member"}
 | 
				
			||||||
                value="member"
 | 
					              onChange={handleRoleChange}
 | 
				
			||||||
                checked={selectedRole === "member"}
 | 
					              className="m-2 hover:cursor-pointer"
 | 
				
			||||||
                onChange={handleRoleChange}
 | 
					            />
 | 
				
			||||||
                className="ml-2 mr-2"
 | 
					            Member
 | 
				
			||||||
              />
 | 
					          </label>
 | 
				
			||||||
              Member
 | 
					 | 
				
			||||||
            </label>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          text="Change"
 | 
					          text="Change"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								frontend/src/Components/Inputs/DescriptionInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/src/Components/Inputs/DescriptionInput.tsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								frontend/src/Components/Inputs/PasswordInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								frontend/src/Components/Inputs/PasswordInput.tsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								frontend/src/Components/Inputs/ProjectNameInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/Components/Inputs/ProjectNameInput.tsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								frontend/src/Components/Inputs/UsernameInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								frontend/src/Components/Inputs/UsernameInput.tsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										24
									
								
								frontend/src/Components/NavButton.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/Components/NavButton.tsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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,44 +69,88 @@ 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>
 | 
					          >
 | 
				
			||||||
          </div>
 | 
					            (Change project name)
 | 
				
			||||||
          <div className="border-2 border-black rounded-lg h-[8vh] text-left divide-y-2 flex flex-col overflow-auto mx-10">
 | 
					          </p>
 | 
				
			||||||
            <p className="p-2">Number of members: {users.length}</p>
 | 
					          {showInput && (
 | 
				
			||||||
            <p className="p-2">
 | 
					            <>
 | 
				
			||||||
 | 
					              <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>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <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) => {
 | 
				
			||||||
                <li
 | 
					                setSearch(e.target.value);
 | 
				
			||||||
                  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"
 | 
					              }}
 | 
				
			||||||
                  key={user.Username}
 | 
					            />
 | 
				
			||||||
                  onClick={() => {
 | 
					            <ul className="border-2 border-black mt-2 p-2 rounded-2xl text-left overflow-auto h-[24vh] font-medium space-y-2">
 | 
				
			||||||
                    props.onClick(user.Username);
 | 
					              {users
 | 
				
			||||||
                  }}
 | 
					                .filter((user) => {
 | 
				
			||||||
                >
 | 
					                  return search.toLowerCase() === ""
 | 
				
			||||||
                  <span>
 | 
					                    ? user.Username
 | 
				
			||||||
                    Name: {user.Username}
 | 
					                    : user.Username.toLowerCase().includes(
 | 
				
			||||||
                    <div></div>
 | 
					                        search.toLowerCase(),
 | 
				
			||||||
                    Role: {user.UserRole}
 | 
					                      );
 | 
				
			||||||
                  </span>
 | 
					                })
 | 
				
			||||||
                </li>
 | 
					                .map((user) => (
 | 
				
			||||||
              ))}
 | 
					                  <li
 | 
				
			||||||
 | 
					                    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}
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      props.onClick(user.Username, user.UserRole);
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <span>
 | 
				
			||||||
 | 
					                      Name: {user.Username}
 | 
				
			||||||
 | 
					                      <div></div>
 | 
				
			||||||
 | 
					                      Role: {user.UserRole}
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
            </ul>
 | 
					            </ul>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="space-x-5 my-2">
 | 
					          <div className="space-x-5 my-2">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,21 +62,36 @@ 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"}
 | 
				
			||||||
            <li
 | 
					          type={"Text"}
 | 
				
			||||||
              className="pt-5"
 | 
					          value={search}
 | 
				
			||||||
              key={project.name}
 | 
					          onChange={(e) => {
 | 
				
			||||||
              onClick={() => {
 | 
					            setSearch(e.target.value);
 | 
				
			||||||
                handleClickProject(project.name);
 | 
					          }}
 | 
				
			||||||
              }}
 | 
					        />
 | 
				
			||||||
            >
 | 
					        <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]">
 | 
				
			||||||
              {project.name}
 | 
					          {props.projects
 | 
				
			||||||
            </li>
 | 
					            .filter((project) => {
 | 
				
			||||||
          ))}
 | 
					              return search.toLowerCase() === ""
 | 
				
			||||||
 | 
					                ? project.name
 | 
				
			||||||
 | 
					                : project.name.toLowerCase().includes(search.toLowerCase());
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .map((project) => (
 | 
				
			||||||
 | 
					              <li
 | 
				
			||||||
 | 
					                className="hover:font-extrabold hover:underline p-1"
 | 
				
			||||||
 | 
					                key={project.name}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  handleClickProject(project.name);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {project.name}
 | 
				
			||||||
 | 
					              </li>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"
 | 
					            onChange={(e) => {
 | 
				
			||||||
              value={username ?? ""}
 | 
					              setUsername(e.target.value);
 | 
				
			||||||
              onChange={(e) => {
 | 
					            }}
 | 
				
			||||||
                setUsername(e.target.value);
 | 
					          />
 | 
				
			||||||
              }}
 | 
					          <div className="py-2" />
 | 
				
			||||||
            />
 | 
					          <PasswordInput
 | 
				
			||||||
            <InputField
 | 
					            password={password}
 | 
				
			||||||
              label="Password"
 | 
					            onChange={(e) => {
 | 
				
			||||||
              type="password"
 | 
					              setPassword(e.target.value);
 | 
				
			||||||
              value={password ?? ""}
 | 
					            }}
 | 
				
			||||||
              onChange={(e) => {
 | 
					          />
 | 
				
			||||||
                setPassword(e.target.value);
 | 
					
 | 
				
			||||||
              }}
 | 
					          <div className="flex self-center justify-between">
 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div className="flex items-center justify-between">
 | 
					 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
              text="Register"
 | 
					              text="Register"
 | 
				
			||||||
              onClick={(): void => {
 | 
					              onClick={(): void => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 => {
 | 
				
			||||||
    const nameChange: StrNameChange = {
 | 
					    if (
 | 
				
			||||||
      prevName: props.username,
 | 
					      !alphanumeric.test(newUsername) ||
 | 
				
			||||||
      newName: newUsername.replace(/ /g, ""),
 | 
					      newUsername.length > usernameUpLimit ||
 | 
				
			||||||
    };
 | 
					      newUsername.length < usernameLowLimit
 | 
				
			||||||
    ChangeUsername({ nameChange: nameChange });
 | 
					    ) {
 | 
				
			||||||
 | 
					      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 = {
 | 
				
			||||||
 | 
					        prevName: props.username,
 | 
				
			||||||
 | 
					        newName: newUsername.replace(/ /g, ""),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      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={
 | 
				
			||||||
            (Change Username)
 | 
					                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}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              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"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,24 +30,39 @@ 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"}
 | 
				
			||||||
            <li
 | 
					          type={"Text"}
 | 
				
			||||||
              className="pt-5"
 | 
					          value={search}
 | 
				
			||||||
              key={user}
 | 
					          onChange={(e) => {
 | 
				
			||||||
              onClick={() => {
 | 
					            setSearch(e.target.value);
 | 
				
			||||||
                handleClick(user);
 | 
					          }}
 | 
				
			||||||
              }}
 | 
					        />
 | 
				
			||||||
            >
 | 
					        <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]">
 | 
				
			||||||
              {user}
 | 
					          {props.users
 | 
				
			||||||
            </li>
 | 
					            .filter((user) => {
 | 
				
			||||||
          ))}
 | 
					              return search.toLowerCase() === ""
 | 
				
			||||||
 | 
					                ? user
 | 
				
			||||||
 | 
					                : user.toLowerCase().includes(search.toLowerCase());
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .map((user) => (
 | 
				
			||||||
 | 
					              <li
 | 
				
			||||||
 | 
					                className="hover:font-extrabold hover:underline p-1"
 | 
				
			||||||
 | 
					                key={user}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  handleClick(user);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {user}
 | 
				
			||||||
 | 
					              </li>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										36
									
								
								frontend/src/Data/constants.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/Data/constants.ts
									
										
									
									
									
										Normal 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;
 | 
				
			||||||
							
								
								
									
										9
									
								
								frontend/src/Data/regex.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/Data/regex.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Only alphanumerical characters
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const alphanumeric = /^[a-zA-Z0-9]+$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Only lowercase letters
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const lowercase = /^[a-z]+$/;
 | 
				
			||||||
| 
						 | 
					@ -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"} />
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 = (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 />,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue