Compare commits

...

138 commits

Author SHA1 Message Date
Mattias
5803c7b29b Refactor fetchWeeklyReport function, updated submit button text, week fetched by params 2024-03-19 11:41:50 +01:00
Davenludd
d0d0e311e5 Add project name to UserViewTimeReportsPage and add AllTimeReportsInProject component 2024-03-19 11:10:47 +01:00
Davenludd
03819aff44 Update link to time reports in UserProjectPage 2024-03-19 11:10:02 +01:00
Davenludd
7fd128e1f3 Add AllTimeReportsInProject component 2024-03-19 11:09:52 +01:00
Davenludd
09a7cbced1 Update routes in main.tsx 2024-03-19 11:09:35 +01:00
Davenludd
ee2fb6d543 Minor change UserProjectPage 2024-03-19 09:41:24 +01:00
Davenludd
a30a6a4988 Fix button UserNewTimeReportPage 2024-03-19 09:37:13 +01:00
Mattias
cda2b72d08 projectName is now fetched from params 2024-03-19 09:35:15 +01:00
Davenludd
4f53f9de94 Fix initial value of week state in NewWeeklyReport component for error message 2024-03-19 09:33:20 +01:00
Davenludd
53f468d7c8 Update navigation in EditWeeklyReport component 2024-03-19 09:23:09 +01:00
Davenludd
d775a6e381 Update navigation in NewWeeklyReport component 2024-03-19 09:21:33 +01:00
Davenludd
08532444f0 Cleanup YourProjectsPage 2024-03-19 09:14:30 +01:00
al8763be
3b8b9bb3f2 Merge branch 'dev' into frontend 2024-03-19 03:44:23 +01:00
al8763be
3df9ddcd4b Merge remote-tracking branch 'origin/gruppPP' into frontend 2024-03-19 03:44:02 +01:00
pavel Hamawand
cbb62438c8 implementing AdminChangeUsername 2024-03-19 03:40:17 +01:00
pavel Hamawand
8b7ad8911b implementing ChangeUser 2024-03-19 03:39:05 +01:00
Imbus
2ce1837223 Merge branch 'frontend' into dev 2024-03-19 03:36:42 +01:00
pavel Hamawand
4df8d3f858 new component 2024-03-19 03:22:54 +01:00
pavel Hamawand
52aecd14d4 minor fix 2024-03-19 03:14:14 +01:00
al8763be
502cd67b4c Merge branch 'dev' into BumBranch 2024-03-19 02:15:33 +01:00
Imbus
4dbbee3249 Checking errors from transactions in go 2024-03-19 02:14:09 +01:00
al8763be
e3fd9f52ca Merge branch 'gruppPP' into BumBranch 2024-03-19 02:12:37 +01:00
al8763be
8081f289b5 fixed NewWeeklyReport 2024-03-19 02:11:47 +01:00
Peter KW
cdbd6ca0ce Small fixes to design 2024-03-19 01:48:01 +01:00
Imbus
7db03e8dbd Merge branch 'imbs' into dev 2024-03-19 01:43:10 +01:00
Imbus
5f88e92415 Merge branch 'imbs' of git.silversoft.se:Imbus/TTime into imbs 2024-03-19 01:40:36 +01:00
borean
3125b511bb correcting AddProject 2024-03-19 01:38:40 +01:00
Imbus
09014c6659 Better test feedback in python script 2024-03-19 01:38:39 +01:00
Imbus
e498f0ed63 Silencing python testing, optional verbose output 2024-03-19 01:38:39 +01:00
al8763be
77a24421e9 Merge branch 'gruppdm' into BumBranch 2024-03-19 01:24:55 +01:00
al8763be
de234c12f2 Merge branch 'imbs' into BumBranch 2024-03-19 01:24:38 +01:00
Imbus
68b01f2144 Merge branch 'borean' of github.com:imbus64/TTime into borean 2024-03-19 01:23:18 +01:00
borean
58d9001be3 AddProject changed so that user is promoted to projectmanager 2024-03-19 01:23:07 +01:00
Imbus
5bcca0202b Better test feedback in python script 2024-03-19 01:12:14 +01:00
borean
0217f2b512 AddProject changed so that user is promoted to projectmanager 2024-03-19 01:10:02 +01:00
pavel Hamawand
6a84b1c14d fix backbutton 2024-03-19 01:03:38 +01:00
pavel Hamawand
c072aff9da added change username button 2024-03-19 01:02:39 +01:00
al8763be
9434c31013 Merge remote-tracking branch 'origin/dev' into frontend 2024-03-19 00:48:13 +01:00
Imbus
c03be8c5d9 Path rename in python testing script 2024-03-19 00:46:28 +01:00
Davenludd
ba2bb1fc5e Merge branch 'frontend' into gruppDM 2024-03-19 00:43:25 +01:00
al8763be
847427a543 Merge remote-tracking branch 'origin/dev' into frontend 2024-03-19 00:42:32 +01:00
Imbus
b174ec8922 Rename for clarity and consistensy with TS api 2024-03-19 00:41:30 +01:00
Davenludd
b8c69fabf5 Remove unused variable and update API call in YourProjectsPage.tsx 2024-03-19 00:35:27 +01:00
pavel Hamawand
d2a8399bde minor fix 2024-03-19 00:35:15 +01:00
Imbus
2cfbcd15a7 Merge branch 'docs' of github.com:imbus64/TTime into docs 2024-03-19 00:34:15 +01:00
Samuel Högbom Aronson
9ce70e74e9 tyding up and tryinng to get tokens in docs to work 2024-03-19 00:33:38 +01:00
Samuel Högbom Aronson
3e35586bbe finished docs for user reletaded stucts and added endpoint for listallusers 2024-03-19 00:33:38 +01:00
Peter KW
b3dfbc47a4 Merge branch 'frontend' into gruppPP 2024-03-19 00:33:30 +01:00
Imbus
7e4e35f597 Silencing python testing, optional verbose output 2024-03-19 00:31:15 +01:00
Davenludd
17c30f5dd9 Merge branch 'frontend' into gruppDM 2024-03-19 00:28:12 +01:00
al8763be
d7e14f1886 quick fix for getUserProjects API 2024-03-19 00:27:35 +01:00
Samuel Högbom Aronson
8711f9a20d tyding up and tryinng to get tokens in docs to work 2024-03-19 00:27:31 +01:00
al8763be
59c4dab2e2 quick fix for getUserProjects API 2024-03-19 00:26:17 +01:00
Peter KW
d2ff2428cd Some corrections 2024-03-19 00:26:05 +01:00
Peter KW
36524e5cbb Changed so that it makes a modal for each user instead of a link 2024-03-19 00:25:37 +01:00
Davenludd
a2bc13ec22 Merge branch 'frontend' into gruppDM 2024-03-19 00:20:43 +01:00
al8763be
83f8097c2b API getUserProjects Fucked 2024-03-19 00:20:08 +01:00
Peter KW
a0759b099a Modul for viewing user info in admin manage users page 2024-03-19 00:17:29 +01:00
Peter KW
db4f869712 Delete user component 2024-03-19 00:15:42 +01:00
Davenludd
652f74884f Merge branch 'frontend' into gruppDM 2024-03-19 00:15:41 +01:00
al8763be
3e1f899414 Merge branch 'dev' into frontend 2024-03-19 00:14:55 +01:00
Peter KW
e55b380bb4 Should be able to delete users except for self now 2024-03-19 00:14:55 +01:00
Imbus
2ffbc2f9fd Merge docs -> dev 2024-03-19 00:10:42 +01:00
Imbus
fe08d01e15 Insanely verbose logging 2024-03-19 00:00:18 +01:00
Imbus
71caf37642 Resolve testing.py 2024-03-18 23:43:14 +01:00
Imbus
108850c20c Merge imbs -> dev 2024-03-18 23:40:56 +01:00
Imbus
4c297ab2ef Old unresolved merge conflict fixed 2024-03-18 23:39:02 +01:00
Imbus
0fa7558d64 Proper logging, all endpoint debug printing replaced with suitable log level 2024-03-18 23:38:50 +01:00
Davenludd
2eab030212 Merge branch 'frontend' into gruppDM 2024-03-18 23:37:48 +01:00
Davenludd
ff9eba039f Minor fixes YourProjectsPage 2024-03-18 23:36:59 +01:00
al8763be
2cd2ef9ef5 Merge branch 'frontend' of https://github.com/imbus64/TTime into frontend 2024-03-18 23:36:50 +01:00
al8763be
9056aafd2e Test for getUserProjectsAdded 2024-03-18 23:34:03 +01:00
Samuel Högbom Aronson
ad85194d4f finished docs for user reletaded stucts and added endpoint for listallusers 2024-03-18 23:21:49 +01:00
al8763be
2cff1d55f9 Fixed migration data 2024-03-18 23:08:38 +01:00
Imbus
7932350980 Exit with non-zero if migration fails 2024-03-18 23:04:08 +01:00
Davenludd
cc09eb0ead Remove duplicate import statements 2024-03-18 23:01:30 +01:00
Davenludd
8df3311f5a Remove duplicate code in UserProjectPage 2024-03-18 22:59:16 +01:00
al8763be
bed9381509 Merge branch 'BumBranch' into dev 2024-03-18 22:48:23 +01:00
al8763be
95b09a8ce7 Fixed getProjectForUsers 2024-03-18 22:46:53 +01:00
Samuel Högbom Aronson
f3c5abf4f3 added docs for loginrenew, login, regsiter 2024-03-18 22:40:51 +01:00
al8763be
a5399c9335 Samle data without query 2024-03-18 22:39:43 +01:00
al8763be
7ae6cce6b4 Sample data 2024-03-18 22:39:02 +01:00
Imbus
472940cedc Remove index from userId 2024-03-18 22:20:25 +01:00
Imbus
f5a914330f Removed userId identifier from user table, introducing numerous errors 2024-03-18 22:10:19 +01:00
Imbus
c31f145c35 Database sample data, make target and go code 2024-03-18 22:07:02 +01:00
Davenludd
f5a4c3d0e5 Merge branch 'frontend' into gruppDM 2024-03-18 22:05:50 +01:00
Davenludd
55fd42090d Remove username prop from BasicWindow component on all pages 2024-03-18 22:00:58 +01:00
Davenludd
5a4049eaf3 Remove username prop from BasicWindow component 2024-03-18 21:56:37 +01:00
Davenludd
59add3b6b3 Remove username prop from BasicWindow component 2024-03-18 21:55:47 +01:00
Davenludd
6982d21016 Add project name to URL in UserProjectPage 2024-03-18 21:55:15 +01:00
Davenludd
f16dc1722c Update project name in YourProjectsPage.tsx URL 2024-03-18 21:53:16 +01:00
Davenludd
31c5a78dae Refactor routing paths in main.tsx 2024-03-18 21:52:34 +01:00
Davenludd
93addc9870 Fixes in NewWeeklyReport component 2024-03-18 21:40:07 +01:00
Davenludd
847180cf75 Update user navigation route 2024-03-18 21:37:52 +01:00
Davenludd
b9d7e57f2c Update background image in Header component 2024-03-18 21:37:31 +01:00
Davenludd
25713443e2 Remove username prop from BasicWindow component 2024-03-18 21:33:20 +01:00
Imbus
47b60038b4 Merge branch 'frontend' into dev 2024-03-18 21:24:42 +01:00
Imbus
e0de61dd94 Type fixes in frontend, Register & YourProjectsPage 2024-03-18 21:24:26 +01:00
Imbus
f437b25da5 Merge branch 'frontend' into dev 2024-03-18 21:18:49 +01:00
al8763be
8eb23bf7f9 lint bro happ + test for getUserProject 2024-03-18 21:08:33 +01:00
Imbus
4683dd459a Remove code related to demo button in backend 2024-03-18 20:56:00 +01:00
Imbus
2be4afd0e0 Correct ish swagger docstring 2024-03-18 20:05:47 +01:00
Imbus
2aade5d2fe Docs example 2024-03-18 19:59:14 +01:00
Davenludd
d64ec708a1 Minor fixes 2024-03-18 19:37:37 +01:00
Davenludd
3e9dc87100 Add NotFoundPage to handle 404 errors 2024-03-18 19:36:28 +01:00
Davenludd
a2ad2913e4 Add NotFoundPage component 2024-03-18 19:34:15 +01:00
Peter KW
83e781c877 Fixed getting the username and removed comment 2024-03-18 18:31:58 +01:00
al8763be
4392b68397 Removed duplicate getUserProjects 2024-03-18 17:40:53 +01:00
Davenludd
531e9a0535 Fix links in UserProjectPage component 2024-03-18 17:38:45 +01:00
al8763be
d0cc6f2c1b Merge remote-tracking branch 'origin/dev' into BumBranch 2024-03-18 17:35:19 +01:00
al8763be
805d05f8a5 Merge branch 'gruppPP' into BumBranch 2024-03-18 17:31:03 +01:00
Imbus
fe6942aa81 Merge imbs -> dev 2024-03-18 17:30:50 +01:00
al8763be
e5904253e3 Merge branch 'gruppdm' into BumBranch 2024-03-18 17:28:57 +01:00
Peter KW
d692165f99 Removed username prop from basicwindow in all pages 2024-03-18 17:27:32 +01:00
Peter KW
388a430613 Tiny fix 2024-03-18 17:22:40 +01:00
Peter KW
2493932f77 Removed username prop, no longer used 2024-03-18 17:20:29 +01:00
al8763be
fbf46b7cd0 Merge branch 'frontend' into BumBranch 2024-03-18 17:14:23 +01:00
pavel Hamawand
b93ef48500 minor fix 2024-03-18 17:07:48 +01:00
pavel Hamawand
0f7f866cde minor fix 2024-03-18 17:06:05 +01:00
pavel Hamawand
3ec0d168eb minor fix 2024-03-18 17:05:37 +01:00
pavel Hamawand
e47b251c14 implement component 2024-03-18 17:03:02 +01:00
pavel Hamawand
39983c7f6f Render Project List 2024-03-18 17:01:11 +01:00
pavel Hamawand
f61ef87d5e Fetch Projects from API - needs fixing 2024-03-18 16:58:46 +01:00
pavel Hamawand
90afe80408 Implement State management 2024-03-18 16:54:16 +01:00
pavel Hamawand
4173003d32 initial component Setup 2024-03-18 16:53:13 +01:00
pavel Hamawand
b8f669e454 new component UserProjectListAdmin 2024-03-18 16:52:40 +01:00
pavel Hamawand
22b9580f51 fix backbutton 2024-03-18 16:52:40 +01:00
pavel Hamawand
8291f4caf3 delete component 2024-03-18 16:52:40 +01:00
pavel Hamawand
576a137038 new component 2024-03-18 16:52:40 +01:00
Peter KW
ace11570a5 Merge branch 'gruppDM' into gruppPP 2024-03-18 16:49:02 +01:00
Peter KW
2bd9878359 Merge branch 'frontend' into gruppPP 2024-03-18 16:44:13 +01:00
Imbus
0c2617d0cb Full fix for getProject route, testing, integration testing and frontend-API code 2024-03-18 16:42:35 +01:00
dDogge
9ad89d6063 Handler for SignReport added and corresponding test in testing.pu added 2024-03-18 16:01:51 +01:00
borean
3a3690e3da ignore go.work.sum 2024-03-18 15:12:11 +01:00
dDogge
4979378779 Handler for AddUserToProject and promoteToAdmin, successfully tested 2024-03-18 14:47:15 +01:00
al8763be
d6ce4a3c57 errMessage fixed 2024-03-18 14:43:28 +01:00
pavel Hamawand
409d973d8a minor fix 2024-03-18 14:35:56 +01:00
dDogge
76fefd2b24 Added function to check if someone is admin 2024-03-18 13:32:55 +01:00
57 changed files with 1383 additions and 274 deletions

1
.gitignore vendored
View file

@ -36,6 +36,7 @@ dist/
.vscode/
.idea/
.DS_Store
.go.work.sum
# Ignore configuration files
.env

View file

@ -10,6 +10,7 @@ DB_FILE = db.sqlite3
# Directory containing migration SQL scripts
MIGRATIONS_DIR = internal/database/migrations
SAMPLE_DATA_DIR = internal/database/sample_data
# Build target
build:
@ -54,6 +55,14 @@ migrate:
sqlite3 $(DB_FILE) < $$file; \
done
sampledata:
@echo "If this ever fails, run make clean and try again"
@echo "Migrating database $(DB_FILE) using SQL scripts in $(SAMPLE_DATA_DIR)"
@for file in $(wildcard $(SAMPLE_DATA_DIR)/*.sql); do \
echo "Applying migration: $$file"; \
sqlite3 $(DB_FILE) < $$file; \
done
# Target added primarily for CI/CD to ensure that the database is created before running tests
db.sqlite3:
make migrate

View file

@ -19,19 +19,174 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/register": {
"/login": {
"post": {
"description": "logs the user in and returns a jwt token",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "login",
"parameters": [
{
"description": "login info",
"name": "NewUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.NewUser"
}
}
],
"responses": {
"200": {
"description": "Successfully signed token for user",
"schema": {
"type": "Token"
}
},
"400": {
"description": "Bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/loginerenew": {
"post": {
"security": [
{
"bererToken": []
}
],
"description": "renews the users token",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "LoginRenews",
"responses": {
"200": {
"description": "Successfully signed token for user",
"schema": {
"type": "Token"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/promoteToAdmin": {
"post": {
"description": "promote chosen user to admin",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "PromoteToAdmin",
"parameters": [
{
"description": "user info",
"name": "NewUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.NewUser"
}
}
],
"responses": {
"200": {
"description": "Successfully prometed user",
"schema": {
"type": "json"
}
},
"400": {
"description": "bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/register": {
"post": {
"description": "Register a new user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
"text/plain"
],
"tags": [
"User"
],
"summary": "Register a new user",
"summary": "Register",
"parameters": [
{
"description": "User to register",
"name": "NewUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.NewUser"
}
}
],
"responses": {
"200": {
"description": "User added",
@ -53,6 +208,102 @@ const docTemplate = `{
}
}
}
},
"/userdelete/{username}": {
"delete": {
"description": "UserDelete deletes a user from the database",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "UserDelete",
"responses": {
"200": {
"description": "User deleted",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"403": {
"description": "You can only delete yourself",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/users/all": {
"get": {
"description": "lists all users",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "ListsAllUsers",
"responses": {
"200": {
"description": "Successfully signed token for user",
"schema": {
"type": "json"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {
"types.NewUser": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"bererToken": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"externalDocs": {

View file

@ -20,6 +20,7 @@ type Database interface {
GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error
Migrate() error
MigrateSampleData() 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
AddUserToProject(username string, projectname string, role string) error
@ -32,6 +33,7 @@ type Database interface {
GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error)
}
// This struct is a wrapper type that holds the database connection
@ -48,6 +50,9 @@ type UserProjectMember struct {
//go:embed migrations
var scripts embed.FS
//go:embed sample_data
var sampleData embed.FS
// TODO: Possibly break these out into separate files bundled with the embed package?
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?"
@ -59,9 +64,10 @@ const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username
const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?"
const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id
FROM projects JOIN user_roles ON projects.id = user_roles.project_id
JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;`
const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p
JOIN user_roles ur ON p.id = ur.project_id
JOIN users u ON ur.user_id = u.id
WHERE u.username = ?`
// DbConnect connects to the database
func DbConnect(dbpath string) Database {
@ -106,7 +112,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) {
// GetProject retrieves a specific project by its ID.
func (d *Db) GetProject(projectId int) (types.Project, error) {
var project types.Project
err := d.Select(&project, "SELECT * FROM projects WHERE id = ?")
err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId)
if err != nil {
println("Error getting project: ", err)
}
return project, err
}
@ -192,7 +201,25 @@ func (d *Db) GetProjectId(projectname string) (int, error) {
// Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error {
_, err := d.Exec(projectInsert, name, description, username)
tx := d.MustBegin()
_, err := tx.Exec(projectInsert, name, description, username)
if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
_, err = tx.Exec(changeUserRole, "project_manager", username, name)
if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return err
}
@ -313,6 +340,26 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err
}
// IsSiteAdmin checks if a given username is a site admin
func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Define the SQL query to check if the user is a site admin
query := `
SELECT COUNT(*) FROM site_admin
JOIN users ON site_admin.admin_id = users.id
WHERE users.username = ?
`
// Execute the query
var count int
err := d.Get(&count, query, username)
if err != nil {
return false, err
}
// If count is greater than 0, the user is a site admin
return count > 0, nil
}
// Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory
func (d *Db) Migrate() error {
@ -354,3 +401,42 @@ func (d *Db) Migrate() error {
return nil
}
// MigrateSampleData applies sample data to the database.
func (d *Db) MigrateSampleData() error {
// Insert sample data
files, err := sampleData.ReadDir("sample_data")
if err != nil {
return err
}
if len(files) == 0 {
println("No sample data files found")
}
tr := d.MustBegin()
// Iterate over each SQL file and execute it
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".sql" {
continue
}
// This is perhaps not the most elegant way to do this
sqlBytes, err := sampleData.ReadFile("sample_data/" + file.Name())
if err != nil {
return err
}
sqlQuery := string(sqlBytes)
_, err = tr.Exec(sqlQuery)
if err != nil {
return err
}
}
if tr.Commit() != nil {
return err
}
return nil
}

View file

@ -536,3 +536,33 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't")
}
}
func TestGetProject(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Retrieve the added project
project, err := db.GetProject(1)
if err != nil {
t.Error("GetProject failed:", err)
}
// Check if the retrieved project matches the expected values
if project.Name != "testproject" {
t.Errorf("Expected Name to be testproject, got %s", project.Name)
}
}

View file

@ -4,11 +4,9 @@
-- password is the hashed password
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
-- Users are commonly searched by username and userId
CREATE INDEX IF NOT EXISTS users_username_index ON users (username);
CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId);

View file

@ -0,0 +1,35 @@
INSERT OR IGNORE INTO users(username, password)
VALUES ("admin", "123");
INSERT OR IGNORE INTO users(username, password)
VALUES ("user", "123");
INSERT OR IGNORE INTO users(username, password)
VALUES ("user2", "123");
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest","test project", 1);
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest2","test project2", 1);
INSERT OR IGNORE INTO projects(name,description,owner_user_id)
VALUES ("projecttest3","test project3", 1);
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,1,"project_manager");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (2,1,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,1,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,2,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (3,3,"member");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (2,1,"project_manager");

View file

@ -17,6 +17,9 @@ type GlobalState interface {
SubmitWeeklyReport(c *fiber.Ctx) error
GetWeeklyReport(c *fiber.Ctx) error
SignReport(c *fiber.Ctx) error
GetProject(c *fiber.Ctx) error
AddUserToProjectHandler(c *fiber.Ctx) error
PromoteToAdmin(c *fiber.Ctx) error
// GetProject(c *fiber.Ctx) error // To get a specific project
// UpdateProject(c *fiber.Ctx) error // To update a project
// DeleteProject(c *fiber.Ctx) error // To delete a project
@ -31,29 +34,17 @@ type GlobalState interface {
// UpdateCollection(c *fiber.Ctx) error // To update a collection
// DeleteCollection(c *fiber.Ctx) error // To delete a collection
// SignCollection(c *fiber.Ctx) error // To sign a collection
GetButtonCount(c *fiber.Ctx) error // For demonstration purposes
IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes
ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database
ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project
ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project
ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database
ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project
ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project
}
// "Constructor"
func NewGlobalState(db database.Database) GlobalState {
return &GState{Db: db, ButtonCount: 0}
return &GState{Db: db}
}
// The global state, which implements all the handlers
type GState struct {
Db database.Database
ButtonCount int
}
func (gs *GState) GetButtonCount(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
}
func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error {
gs.ButtonCount++
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
Db database.Database
}

View file

@ -5,6 +5,7 @@ import (
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
@ -66,33 +67,90 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
if projectID == "" {
log.Info("No project ID provided")
return c.Status(400).SendString("No project ID provided")
}
log.Info("Getting project with ID: ", projectID)
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
log.Info("Invalid project ID")
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
log.Info("Error getting project:", err)
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
log.Info("Returning project: ", project.Name)
return c.JSON(project)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
if projectName == "" {
log.Info("No project name provided")
return c.Status(400).SendString("No project name provided")
}
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users for project:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning users for project: ", projectName)
// Return the list of users as JSON
return c.JSON(users)
}
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
// Extract necessary parameters from the request
var requestData struct {
Username string `json:"username"`
ProjectName string `json:"projectName"`
Role string `json:"role"`
}
if err := c.BodyParser(&requestData); err != nil {
log.Info("Error parsing request body:", err)
return c.Status(400).SendString("Bad request")
}
// Check if the user adding another user to the project is a site admin
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
adminUsername := claims["name"].(string)
log.Info("Admin username from claims:", adminUsername)
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is not a site admin:", adminUsername)
return c.Status(403).SendString("User is not a site admin")
}
// Add the user to the project with the specified role
err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role)
if err != nil {
log.Info("Error adding user to project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User added to project successfully:", requestData.Username)
return c.SendStatus(fiber.StatusOK)
}

View file

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

View file

@ -4,39 +4,54 @@ import (
"time"
"ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// Register is a simple handler that registers a new user
//
// @Summary Register a new user
// @Summary Register
// @Description Register a new user
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /api/register [post]
// @Produce plain
// @Param NewUser body types.NewUser true "User to register"
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /register [post]
func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
println("Error parsing body")
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
println("Adding user:", u.Username)
log.Info("Adding user:", u.Username)
if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
log.Warn("Error adding user:", err)
return c.Status(500).SendString(err.Error())
}
println("User added:", u.Username)
log.Info("User added:", u.Username)
return c.Status(200).SendString("User added")
}
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
//
// @Summary UserDelete
// @Description UserDelete deletes a user from the database
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 {string} string "User deleted"
// @Failure 403 {string} string "You can only delete yourself"
// @Failure 500 {string} string "Internal server error"
// @Failure 401 {string} string "Unauthorized"
// @Router /userdelete/{username} [delete]
func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")
@ -45,28 +60,44 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error {
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username != auth_username {
log.Info("User tried to delete another user")
return c.Status(403).SendString("You can only delete yourself")
}
if err := gs.Db.RemoveUser(username); err != nil {
log.Warn("Error deleting user:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("User deleted:", username)
return c.Status(200).SendString("User deleted")
}
// Login is a simple login handler that returns a JWT token
//
// @Summary login
// @Description logs the user in and returns a jwt token
// @Tags User
// @Accept json
// @Param NewUser body types.NewUser true "login info"
// @Produce plain
// @Success 200 Token types.Token "Successfully signed token for user"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /login [post]
func (gs *GState) Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
println("Error parsing body")
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
println("Username:", u.Username)
log.Info("Username logging in:", u.Username)
if !gs.Db.CheckUser(u.Username, u.Password) {
println("User not found")
log.Info("User not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
@ -79,23 +110,36 @@ func (gs *GState) Login(c *fiber.Ctx) error {
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
println("Token created for user:", u.Username)
log.Info("Token created for user:", u.Username)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
println("Error signing token")
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
println("Successfully signed token for user:", u.Username)
return c.JSON(fiber.Map{"token": t})
return c.JSON(types.Token{Token: t})
}
// LoginRenew is a simple handler that renews the token
//
// @Summary LoginRenews
// @Description renews the users token
// @Security bererToken
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 Token types.Token "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /loginerenew [post]
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token)
log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"])
claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
@ -106,19 +150,67 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])
return c.JSON(types.Token{Token: t})
}
// ListAllUsers is a handler that returns a list of all users in the application database
//
// @Summary ListsAllUsers
// @Description lists all users
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 {json} json "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /users/all [get]
func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := gs.Db.GetAllUsersApplication()
if err != nil {
log.Info("Error getting users from db:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("Returning all users")
// Return the list of users as JSON
return c.JSON(users)
}
// @Summary PromoteToAdmin
// @Description promote chosen user to admin
// @Tags User
// @Accept json
// @Produce plain
// @Param NewUser body types.NewUser true "user info"
// @Success 200 {json} json "Successfully prometed user"
// @Failure 400 {string} string "bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /promoteToAdmin [post]
func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body
var newUser types.NewUser
if err := c.BodyParser(&newUser); err != nil {
return c.Status(400).SendString("Bad request")
}
username := newUser.Username
log.Info("Promoting user to admin:", username) // Debug print
// Promote the user to a site admin in the database
if err := gs.Db.PromoteToAdmin(username); err != nil {
log.Info("Error promoting user to admin:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("User promoted to admin successfully:", username) // Debug print
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -27,3 +27,8 @@ type PublicUser struct {
UserId string `json:"userId"`
Username string `json:"username"`
}
// wrapper type for token
type Token struct {
Token string `json:"token"`
}

View file

@ -10,6 +10,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/swagger"
jwtware "github.com/gofiber/contrib/jwt"
@ -22,6 +23,10 @@ import (
// @license.name AGPL
// @license.url https://www.gnu.org/licenses/agpl-3.0.html
//@securityDefinitions.apikey bererToken
//@in header
//@name Authorization
// @host localhost:8080
// @BasePath /api
@ -46,6 +51,12 @@ func main() {
// Migrate the database
if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err)
os.Exit(1)
}
if err = db.MigrateSampleData(); err != nil {
fmt.Println("Error migrating sample data: ", err)
os.Exit(1)
}
// Get our global state
@ -53,6 +64,9 @@ func main() {
// Create the server
server := fiber.New()
server.Use(logger.New())
// Mounts the swagger documentation, this is available at /swagger/index.html
server.Get("/swagger/*", swagger.HandlerDefault)
// Mount our static files (Beware of the security implications of this!)
@ -61,11 +75,6 @@ func main() {
// Register our unprotected routes
server.Post("/api/register", gs.Register)
// Register handlers for example button count
server.Get("/api/button", gs.GetButtonCount)
server.Post("/api/button", gs.IncrementButtonCount)
server.Post("/api/login", gs.Login)
// Every route from here on will require a valid JWT
@ -73,13 +82,18 @@ func main() {
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
}))
server.Post("/api/submitReport", gs.SubmitWeeklyReport)
// Protected routes (require a valid JWT bearer token authentication header)
server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport)
server.Get("/api/getUserProjects", gs.GetUserProjects)
server.Post("/api/loginrenew", gs.LoginRenew)
server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches
server.Post("/api/project", gs.CreateProject)
server.Get("/api/project/:projectId", gs.GetProject)
server.Get("/api/getWeeklyReport", gs.GetWeeklyReport)
server.Post("/api/signReport", gs.SignReport)
server.Put("/api/addUserToProject", gs.AddUserToProjectHandler)
server.Post("/api/promoteToAdmin", gs.PromoteToAdmin)
server.Get("/api/users/all", gs.ListAllUsers)
// Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port))
if err != nil {

View file

@ -29,11 +29,6 @@ interface API {
project: NewProject,
token: string,
): Promise<APIResponse<Project>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Submit a weekly report */
submitWeeklyReport(
project: NewWeeklyReport,
@ -46,6 +41,10 @@ interface API {
week: string,
token: string,
): Promise<APIResponse<NewWeeklyReport>>;
/** Gets all the projects of a user*/
getUserProjects(token: string): Promise<APIResponse<Project[]>>;
/** Gets a project from id*/
getProject(id: number): Promise<APIResponse<Project>>;
}
// Export an instance of the API
@ -170,7 +169,7 @@ export const api: API = {
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
message: "API fucked",
});
}
},
@ -253,4 +252,30 @@ export const api: API = {
return Promise.resolve({ success: false, message: "Failed to login" });
}
},
// Gets a projet by id, currently untested since we have no javascript-based tests
async getProject(id: number): Promise<APIResponse<Project>> {
try {
const response = await fetch(`/api/project/${id}`, {
method: "GET",
});
if (!response.ok) {
return {
success: false,
message: "Failed to get project: Response code " + response.status,
};
} else {
const data = (await response.json()) as Project;
return { success: true, data };
}
// The code below is garbage but satisfies the linter
// This needs fixing, do not copy this pattern
} catch (e: unknown) {
return {
success: false,
message: "Failed to get project: " + (e as Error).toString(),
};
}
},
};

View file

@ -0,0 +1,109 @@
import React, { useEffect, useState } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { Link, useParams } from "react-router-dom";
function AllTimeReportsInProject(): JSX.Element {
const { projectName } = useParams();
const [weeklyReports, setWeeklyReports] = useState<NewWeeklyReport[]>([]);
/* const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getWeeklyReports(token);
console.log(response);
if (response.success) {
setWeeklyReports(response.data ?? []);
} else {
console.error(response.message);
}
}; */
const getWeeklyReports = async (): Promise<void> => {
const report: NewWeeklyReport[] = [
{
projectName: projectName ?? "",
week: 10,
developmentTime: 1,
meetingTime: 1,
adminTime: 1,
ownWorkTime: 1,
studyTime: 1,
testingTime: 1,
},
{
projectName: projectName ?? "",
week: 11,
developmentTime: 1,
meetingTime: 1,
adminTime: 1,
ownWorkTime: 100,
studyTime: 1,
testingTime: 1,
},
{
projectName: projectName ?? "",
week: 12,
developmentTime: 1,
meetingTime: 1,
adminTime: 1,
ownWorkTime: 1,
studyTime: 1,
testingTime: 1000,
},
{
projectName: projectName ?? "",
week: 20,
developmentTime: 1,
meetingTime: 1,
adminTime: 1,
ownWorkTime: 1,
studyTime: 1,
testingTime: 10000,
},
// Add more reports as needed
];
setWeeklyReports(report);
await Promise.resolve();
};
// Call getProjects when the component mounts
useEffect(() => {
void getWeeklyReports();
}, []);
return (
<>
<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] text-[30px]">
{weeklyReports.map((newWeeklyReport, index) => (
<Link
to={`/editTimeReport/${projectName}/${newWeeklyReport.week}`}
key={index}
className="border-b-2 border-black w-full"
>
<div className="flex justify-between">
<h1>
<span className="font-bold">{"Week: "}</span>
{newWeeklyReport.week}
</h1>
<h1>
<span className="font-bold">{"Total Time: "}</span>
{newWeeklyReport.developmentTime +
newWeeklyReport.meetingTime +
newWeeklyReport.adminTime +
newWeeklyReport.ownWorkTime +
newWeeklyReport.studyTime +
newWeeklyReport.testingTime}{" "}
min
</h1>
<h1>
<span className="font-bold">{"Signed: "}</span>
YES
</h1>
</div>
</Link>
))}
</div>
</>
);
}
export default AllTimeReportsInProject;

View file

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

View file

@ -0,0 +1,38 @@
import React, { useState } from "react";
import { api } from "../API/API";
import InputField from "./InputField";
import BackButton from "./BackButton";
import Button from "./Button";
function ChangeUsername(): JSX.Element {
const [newUsername, setNewUsername] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setNewUsername(e.target.value);
};
const handleSubmit = async (): Promise<void> => {
try {
// Call the API function to update the username
await api.updateUsername(newUsername);
// Optionally, add a success message or redirect the user
} catch (error) {
console.error("Error updating username:", error);
// Optionally, handle the error
}
};
return (
<div>
<InputField
label="New Username"
type="text"
value={newUsername}
onChange={handleChange}
/>
</div>
);
}
export default ChangeUsername;

View file

@ -0,0 +1,34 @@
import { User } from "../Types/goTypes";
import { api, APIResponse } from "../API/API";
/**
* Use to remove a user from the system
* @param props - The username of user to remove
* @returns {boolean} True if removed, false if not
* @example
* const exampleUsername = "user";
* DeleteUser({ usernameToDelete: exampleUsername });
*/
function DeleteUser(props: { usernameToDelete: string }): boolean {
//console.log(props.usernameToDelete); FOR DEBUG
let removed = false;
api
.removeUser(
props.usernameToDelete,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<User>) => {
if (response.success) {
removed = true;
} else {
console.error(response.message);
}
})
.catch((error) => {
console.error("An error occurred during creation:", error);
});
return removed;
}
export default DeleteUser;

View file

@ -1,11 +1,10 @@
import { useState, useEffect } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button";
export default function GetWeeklyReport(): JSX.Element {
const [projectName, setProjectName] = useState("");
const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0);
@ -16,46 +15,48 @@ export default function GetWeeklyReport(): JSX.Element {
const token = localStorage.getItem("accessToken") ?? "";
const username = localStorage.getItem("username") ?? "";
const { projectName } = useParams();
const { fetchedWeek } = useParams();
const fetchWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport(
username,
projectName ?? "",
fetchedWeek?.toString() ?? "0",
token,
);
if (response.success) {
const report: NewWeeklyReport = response.data ?? {
projectName: "",
week: 0,
developmentTime: 0,
meetingTime: 0,
adminTime: 0,
ownWorkTime: 0,
studyTime: 0,
testingTime: 0,
};
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
setAdminTime(report.adminTime);
setOwnWorkTime(report.ownWorkTime);
setStudyTime(report.studyTime);
setTestingTime(report.testingTime);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
};
useEffect(() => {
const fetchWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport(
username,
projectName,
week.toString(),
token,
);
if (response.success) {
const report: NewWeeklyReport = response.data ?? {
projectName: "",
week: 0,
developmentTime: 0,
meetingTime: 0,
adminTime: 0,
ownWorkTime: 0,
studyTime: 0,
testingTime: 0,
};
setProjectName(report.projectName);
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
setAdminTime(report.adminTime);
setOwnWorkTime(report.ownWorkTime);
setStudyTime(report.studyTime);
setTestingTime(report.testingTime);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
};
fetchWeeklyReport();
}, []);
void fetchWeeklyReport();
});
const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
projectName,
projectName: projectName ?? "",
week,
developmentTime,
meetingTime,
@ -82,7 +83,7 @@ export default function GetWeeklyReport(): JSX.Element {
}
e.preventDefault();
void handleNewWeeklyReport();
navigate("/project");
navigate(-1);
}}
>
<div className="flex flex-col items-center">
@ -233,7 +234,7 @@ export default function GetWeeklyReport(): JSX.Element {
</tbody>
</table>
<Button
text="Submit"
text="Submit changes"
onClick={(): void => {
return;
}}

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import backgroundImage from "../assets/1.jpg";
function Header(): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
@ -11,7 +12,7 @@ function Header(): JSX.Element {
return (
<header
className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover"
style={{ backgroundImage: `url('src/assets/1.jpg')` }}
style={{ backgroundImage: `url(${backgroundImage})` }}
>
<Link to="/your-projects">
<img

View file

@ -1,32 +1,31 @@
import { useState, useContext } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { useState } from "react";
import type { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button";
import { ProjectNameContext } from "../Pages/YourProjectsPage";
export default function NewWeeklyReport(): JSX.Element {
const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0);
const [adminTime, setAdminTime] = useState(0);
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const [week, setWeek] = useState<number>(0);
const [developmentTime, setDevelopmentTime] = useState<number>();
const [meetingTime, setMeetingTime] = useState<number>();
const [adminTime, setAdminTime] = useState<number>();
const [ownWorkTime, setOwnWorkTime] = useState<number>();
const [studyTime, setStudyTime] = useState<number>();
const [testingTime, setTestingTime] = useState<number>();
const projectName = useContext(ProjectNameContext);
const { projectName } = useParams();
const token = localStorage.getItem("accessToken") ?? "";
const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
projectName,
week,
developmentTime,
meetingTime,
adminTime,
ownWorkTime,
studyTime,
testingTime,
projectName: projectName ?? "",
week: week,
developmentTime: developmentTime ?? 0,
meetingTime: meetingTime ?? 0,
adminTime: adminTime ?? 0,
ownWorkTime: ownWorkTime ?? 0,
studyTime: studyTime ?? 0,
testingTime: testingTime ?? 0,
};
await api.submitWeeklyReport(newWeeklyReport, token);
@ -46,7 +45,7 @@ export default function NewWeeklyReport(): JSX.Element {
}
e.preventDefault();
void handleNewWeeklyReport();
navigate("/project");
navigate(-1);
}}
>
<div className="flex flex-col items-center">
@ -59,7 +58,9 @@ export default function NewWeeklyReport(): JSX.Element {
setWeek(weekNumber);
}}
onKeyDown={(event) => {
event.preventDefault();
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();

View file

@ -23,6 +23,7 @@ export default function Register(): JSX.Element {
nav("/"); // Instantly navigate to the login page
} else {
setErrMessage(response.message ?? "Unknown error");
console.error(errMessage);
}
};
@ -47,7 +48,7 @@ export default function Register(): JSX.Element {
<InputField
label="Username"
type="text"
value={username}
value={username ?? ""}
onChange={(e) => {
setUsername(e.target.value);
}}
@ -55,7 +56,7 @@ export default function Register(): JSX.Element {
<InputField
label="Password"
type="password"
value={password}
value={password ?? ""}
onChange={(e) => {
setPassword(e.target.value);
}}

View file

@ -0,0 +1,54 @@
import { Link } from "react-router-dom";
import Button from "./Button";
import DeleteUser from "./DeleteUser";
import UserProjectListAdmin from "./UserProjectListAdmin";
function UserInfoModal(props: {
isVisible: boolean;
username: string;
onClose: () => void;
}): JSX.Element {
if (!props.isVisible) return <></>;
return (
<div
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white p-2 rounded-lg text-center">
<p className="font-bold text-[30px]">{props.username}</p>
<Link to="/AdminChangeUserName">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Username)
</p>
</Link>
<div>
<h2 className="font-bold text-[22px] mb-[20px]">
Member of these projects:
</h2>
<div className="pr-6 pl-6">
<UserProjectListAdmin />
</div>
</div>
<div className="items-center space-x-6 pr-6 pl-6">
<Button
text={"Delete"}
onClick={function (): void {
DeleteUser({ usernameToDelete: props.username });
}}
type="button"
/>
<Button
text={"Close"}
onClick={function (): void {
props.onClose();
}}
type="button"
/>
</div>
</div>
</div>
);
}
export default UserInfoModal;

View file

@ -1,5 +1,6 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { PublicUser } from "../Types/goTypes";
import UserInfoModal from "./UserInfoModal";
/**
* The props for the UserProps component
@ -9,27 +10,52 @@ interface UserProps {
}
/**
* A list of users for admin manage users page, that links admin to the right user page
* thanks to the state property
* @param props - The users to display
* A list of users for admin manage users page, that sets an onClick
* function for eact user <li> element, which displays a modul with
* user info.
* @param props - An array of users users to display
* @returns {JSX.Element} The user list
* @example
* const users = [{ id: 1, userName: "Random name" }];
* const users = [{ id: 1, userName: "ExampleName" }];
* return <UserList users={users} />;
*/
export function UserListAdmin(props: UserProps): JSX.Element {
const [modalVisible, setModalVisible] = useState(false);
const [username, setUsername] = useState("");
const handleClick = (username: string): void => {
setUsername(username);
setModalVisible(true);
};
const handleClose = (): void => {
setUsername("");
setModalVisible(false);
};
return (
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => (
<Link to="/adminUserInfo" key={user.userId} state={user.username}>
<li className="pt-5" key={user.userId}>
<>
<UserInfoModal
onClose={handleClose}
isVisible={modalVisible}
username={username}
/>
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => (
<li
className="pt-5"
key={user.userId}
onClick={() => {
handleClick(user.username);
}}
>
{user.username}
</li>
</Link>
))}
</ul>
</div>
))}
</ul>
</div>
</>
);
}

View file

@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
import { api } from "../API/API";
import { Project } from "../Types/goTypes";
function UserProjectListAdmin(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
const fetchProjects = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
// const username = props.username;
const response = await api.getUserProjects(token);
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
void fetchProjects();
}, []);
return (
<div className="border-2 border-black bg-white p-2 rounded-lg text-center">
<ul>
{projects.map((project) => (
<li key={project.id}>
<span>{project.name}</span>
</li>
))}
</ul>
</div>
);
}
export default UserProjectListAdmin;

View file

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

View file

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

View file

@ -1,8 +1,14 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import ChangeUsername from "../../Components/ChangeUsername";
function AdminChangeUsername(): JSX.Element {
const content = <></>;
const content = (
<>
<ChangeUsername />
</>
);
const buttons = (
<>
@ -13,16 +19,10 @@ function AdminChangeUsername(): JSX.Element {
}}
type="button"
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminChangeUsername;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ function App(): JSX.Element {
} else if (authority === 2) {
navigate("/pm");
} else if (authority === 3) {
navigate("/user");
navigate("/yourProjects");
}
}, [authority, navigate]);

View file

@ -0,0 +1,18 @@
import Button from "../Components/Button";
export default function NotFoundPage(): JSX.Element {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-white">
<h1 className="text-[30px]">404 Page Not Found</h1>
<a href="/">
<Button
text="Go to Home Page"
onClick={(): void => {
localStorage.clear();
}}
type="button"
/>
</a>
</div>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import TimeReport from "../../Components/NewWeeklyReport";
@ -32,6 +33,6 @@ function PMViewUnsignedReport(): JSX.Element {
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default PMViewUnsignedReport;

View file

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

View file

@ -1,7 +1,6 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import NewWeeklyReport from "../../Components/NewWeeklyReport";
import { Link } from "react-router-dom";
function UserNewTimeReportPage(): JSX.Element {
const content = (
@ -13,18 +12,10 @@ function UserNewTimeReportPage(): JSX.Element {
const buttons = (
<>
<Link to="/project">
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</Link>
<BackButton />
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default UserNewTimeReportPage;

View file

@ -1,18 +1,20 @@
import { Link, useLocation } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
function UserProjectPage(): JSX.Element {
const { projectName } = useParams();
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1>
<h1 className="font-bold text-[40px] mb-[20px]">{projectName}</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]">
<Link to="/project-page">
<Link to={`/timeReports/${projectName}/`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports
</h1>
</Link>
<Link to="/new-time-report">
<Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report
</h1>
@ -27,6 +29,6 @@ function UserProjectPage(): JSX.Element {
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default UserProjectPage;

View file

@ -1,11 +1,17 @@
import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
import { useParams } from "react-router-dom";
import AllTimeReportsInProject from "../../Components/AllTimeReportsInProject";
function UserViewTimeReportsPage(): JSX.Element {
const { projectName } = useParams();
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Your Time Reports</h1>
{/* Här kan du inkludera logiken för att visa användarens tidrapporter */}
<h1 className="font-bold text-[30px] mb-[20px]">
Your Time Reports In: {projectName}
</h1>
<AllTimeReportsInProject />
</>
);
@ -15,6 +21,6 @@ function UserViewTimeReportsPage(): JSX.Element {
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default UserViewTimeReportsPage;

View file

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

View file

@ -29,12 +29,14 @@ import AdminProjectManageMembers from "./Pages/AdminPages/AdminProjectManageMemb
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx";
import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx";
import NotFoundPage from "./Pages/NotFoundPage.tsx";
// This is where the routes are mounted
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <NotFoundPage />,
},
{
path: "/admin",
@ -44,30 +46,26 @@ const router = createBrowserRouter([
path: "/pm",
element: <YourProjectsPage />,
},
{
path: "/user",
element: <YourProjectsPage />,
},
{
path: "/yourProjects",
element: <YourProjectsPage />,
},
{
path: "/editTimeReport",
element: <UserEditTimeReportPage />,
},
{
path: "/newTimeReport",
element: <UserNewTimeReportPage />,
},
{
path: "/project",
path: "/project/:projectName",
element: <UserProjectPage />,
},
{
path: "/projectPage",
path: "/newTimeReport/:projectName",
element: <UserNewTimeReportPage />,
},
{
path: "/timeReports/:projectName",
element: <UserViewTimeReportsPage />,
},
{
path: "/editTimeReport/:projectName/:weekNumber",
element: <UserEditTimeReportPage />,
},
{
path: "/changeRole",
element: <PMChangeRole />,

View file

@ -1,15 +1,28 @@
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View file

@ -2,6 +2,16 @@ import requests
import string
import random
debug_output = False
def gprint(*args, **kwargs):
print("\033[92m", *args, "\033[00m", **kwargs)
print("Running Tests...")
def dprint(*args, **kwargs):
if debug_output:
print(*args, **kwargs)
def randomString(len=10):
"""Generate a random string of fixed length"""
@ -20,43 +30,67 @@ base_url = "http://localhost:8080"
registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/submitReport"
submitReportPath = base_url + "/api/submitWeeklyReport"
getWeeklyReportPath = base_url + "/api/getWeeklyReport"
getProjectPath = base_url + "/api/project"
signReportPath = base_url + "/api/signReport"
addUserToProjectPath = base_url + "/api/addUserToProject"
promoteToAdminPath = base_url + "/api/promoteToAdmin"
getUserProjectsPath = base_url + "/api/getUserProjects"
def test_get_user_projects():
dprint("Testing get user projects")
loginResponse = login("user2", "123")
# Check if the user is added to the project
response = requests.get(
getUserProjectsPath,
json={"username": "user2"},
headers={"Authorization": "Bearer " + loginResponse.json()["token"]},
)
dprint(response.text)
dprint(response.json())
assert response.status_code == 200, "Get user projects failed"
gprint("test_get_user_projects successful")
# Posts the username and password to the register endpoint
def register(username: string, password: string):
print("Registering with username: ", username, " and password: ", password)
dprint("Registering with username: ", username, " and password: ", password)
response = requests.post(
registerPath, json={"username": username, "password": password}
)
print(response.text)
dprint(response.text)
return response
# Posts the username and password to the login endpoint
def login(username: string, password: string):
print("Logging in with username: ", username, " and password: ", password)
dprint("Logging in with username: ", username, " and password: ", password)
response = requests.post(
loginPath, json={"username": username, "password": password}
)
print(response.text)
dprint(response.text)
return response
# Test function to login
def test_login():
response = login(username, "always_same")
assert response.status_code == 200, "Login failed"
print("Login successful")
dprint("Login successful")
gprint("test_login successful")
return response.json()["token"]
# Test function to create a new user
def test_create_user():
response = register(username, "always_same")
assert response.status_code == 200, "Registration failed"
print("Registration successful")
gprint("test_create_user successful")
# Test function to add a project
def test_add_project():
loginResponse = login(username, "always_same")
token = loginResponse.json()["token"]
@ -65,11 +99,11 @@ def test_add_project():
json={"name": projectName, "description": "This is a project"},
headers={"Authorization": "Bearer " + token},
)
print(response.text)
dprint(response.text)
assert response.status_code == 200, "Add project failed"
print("Add project successful")
gprint("test_add_project successful")
# Test function to submit a report
def test_submit_report():
token = login(username, "always_same").json()["token"]
response = requests.post(
@ -86,22 +120,169 @@ def test_submit_report():
},
headers={"Authorization": "Bearer " + token},
)
print(response.text)
dprint(response.text)
assert response.status_code == 200, "Submit report failed"
print("Submit report successful")
gprint("test_submit_report successful")
# Test function to get a weekly report
def test_get_weekly_report():
token = login(username, "always_same").json()["token"]
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName , "week": 1}
params={"username": username, "projectName": projectName, "week": 1},
)
print(response.text)
dprint(response.text)
assert response.status_code == 200, "Get weekly report failed"
gprint("test_get_weekly_report successful")
# Tests getting a project by id
def test_get_project():
token = login(username, "always_same").json()["token"]
response = requests.get(
getProjectPath + "/1", # Assumes that the project with id 1 exists
headers={"Authorization": "Bearer " + token},
)
dprint(response.text)
assert response.status_code == 200, "Get project failed"
gprint("test_get_project successful")
# Test function to add a user to a project
def test_add_user_to_project():
# Log in as a site admin
admin_username = randomString()
admin_password = "admin_password"
dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
dprint(response.text)
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
dprint(response.text)
assert response.status_code == 200, "Promote to site admin failed"
dprint("Admin promoted to site admin successfully")
# Create a new user to add to the project
new_user = randomString()
register(new_user, "new_user_password")
# Add the new user to the project as a member
response = requests.put(
addUserToProjectPath,
json={"projectName": projectName, "username": new_user, "role": "member"},
headers={"Authorization": "Bearer " + admin_token},
)
dprint(response.text)
assert response.status_code == 200, "Add user to project failed"
gprint("test_add_user_to_project successful")
# Test function to sign a report
def test_sign_report():
# Create a project manager user
project_manager = randomString()
register(project_manager, "project_manager_password")
# Register an admin
admin_username = randomString()
admin_password = "admin_password2"
dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
dprint(response.text)
# Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
response = requests.put(
addUserToProjectPath,
json={
"projectName": projectName,
"username": project_manager,
"role": "project_manager",
},
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Add project manager to project failed"
dprint("Project manager added to project successfully")
# Log in as the project manager
project_manager_token = login(project_manager, "project_manager_password").json()[
"token"
]
# Submit a report for the project
token = login(username, "always_same").json()["token"]
response = requests.post(
submitReportPath,
json={
"projectName": projectName,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
headers={"Authorization": "Bearer " + token},
)
assert response.status_code == 200, "Submit report failed"
dprint("Submit report successful")
# Retrieve the report ID
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName, "week": 1},
)
dprint(response.text)
report_id = response.json()["reportId"]
# Sign the report as the project manager
response = requests.post(
signReportPath,
json={"reportId": report_id},
headers={"Authorization": "Bearer " + project_manager_token},
)
assert response.status_code == 200, "Sign report failed"
dprint("Sign report successful")
# Retrieve the report ID again for confirmation
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName, "week": 1},
)
dprint(response.text)
gprint("test_sign_report successful")
if __name__ == "__main__":
test_get_user_projects()
test_create_user()
test_login()
test_add_project()
test_submit_report()
test_get_weekly_report()
test_get_weekly_report()
test_get_project()
test_sign_report()
test_add_user_to_project()