Compare commits

..

201 commits

Author SHA1 Message Date
Peter KW
2f8e7547da Merge branch 'frontend' into gruppPP 2024-04-17 23:06:26 +02:00
Peter KW
3be2319bce ChangeUserPassword.tsx created and implemented + minor fix in API.ts 2024-04-17 23:04:27 +02:00
Peter KW
8948067514 ChangeProjectName.tsx created and implemented + minor fix in main.go api paths 2024-04-17 23:03:32 +02:00
Peter KW
99eb5f17b5 Merge remote-tracking branch 'origin/dev' into gruppPP 2024-04-17 22:29:45 +02:00
al8763be
0176f78067 ChangeUserPassword Handler + API 2024-04-17 22:04:23 +02:00
al8763be
47d4bda99b ChangeProjectName Handler + API 2024-04-17 21:31:47 +02:00
Mattias
97ea6713a4 Update hover effect on menu items and user links 2024-04-15 16:45:45 +02:00
Mattias
2bc3b1b665 Update hover effect on menu items and user links 2024-04-15 16:38:17 +02:00
Mattias
bbac1ed4c8 Added hovering effect to menu items 2024-04-15 16:29:26 +02:00
Mattias
eeb17eabb1 Refactor UserViewStatistics component to remove unnecessary heading 2024-04-15 16:12:56 +02:00
Davenludd
a5a94c7839 Update UserStatistics component to include username parameter in getStatistics API call 2024-04-15 12:02:40 +02:00
Davenludd
128221a6ed Add token parameter to getStatistics API method 2024-04-15 12:02:22 +02:00
Davenludd
90d24ad7c4 Merge branch 'dev' into gruppDM 2024-04-15 11:59:40 +02:00
Imbus
6c4fe7bda3 Statistics api again 2024-04-15 11:58:43 +02:00
Davenludd
cdc070e5a6 Merge branch 'dev' into gruppDM 2024-04-15 11:54:16 +02:00
Imbus
d46c5fc05f Statistics TS API 2024-04-15 11:50:38 +02:00
Davenludd
5033b657a4 Merge branch 'dev' into gruppDM 2024-04-15 11:45:28 +02:00
Imbus
a3f16e713a Statistics 2024-04-15 11:44:19 +02:00
Davenludd
1fd167d412 Merge branch 'dev' into gruppDM 2024-04-15 11:30:17 +02:00
Imbus
b51a00b723 Statistics path rework 2024-04-15 11:09:51 +02:00
Davenludd
e1aefb9934 Update button text 2024-04-14 16:22:17 +02:00
Davenludd
3cd2c7ba40 Update Total Time In Project button text to include project name 2024-04-14 16:12:47 +02:00
Davenludd
f8ca68dbea Update Total Time In Project button text to include username and project name 2024-04-14 16:08:52 +02:00
Davenludd
9148ede977 Update UserViewTimeReportsPage to include statistics-page 2024-04-14 16:03:53 +02:00
Davenludd
78231eaae9 Update routing path 2024-04-14 16:00:29 +02:00
Davenludd
482c732b3d Update UserViewTimeReportsPage to include username in Total Time In Project button link 2024-04-14 15:58:27 +02:00
Davenludd
a25413affa Add total time in TimePerActivity component 2024-04-14 15:52:13 +02:00
Davenludd
d1b6866c39 Fix UserStatistics component to use new API.getStatistics function 2024-04-14 15:47:26 +02:00
Davenludd
c96c0d7b4d Fix order of parameters in getStatistics function in API.ts 2024-04-14 15:47:01 +02:00
Davenludd
97f810fce2 Merge branch 'frontend' into gruppDM 2024-04-14 15:30:37 +02:00
Davenludd
8e60ba405b Update UserViewTimeReportsPage to include Total Time In Project button 2024-04-14 15:25:13 +02:00
Davenludd
6c64dfcb0e Add UserViewStatistics component to UserPages 2024-04-14 15:24:52 +02:00
Davenludd
ac5e67d77f Update button text in PMProjectMembers component 2024-04-14 15:24:42 +02:00
Davenludd
48dd53b56b Add UserStatistics component to show total time per activity in a project 2024-04-14 15:24:10 +02:00
Davenludd
980ac19199 Add UserViewStatistics component to main.tsx 2024-04-14 15:23:42 +02:00
Peter KW
b6d9b51865 Merge branch 'frontend' into gruppPP 2024-04-14 11:58:16 +02:00
Imbus
c7e036ec04 Major db/API refactor, transaction as default 2024-04-14 09:21:34 +02:00
Imbus
fe9d5f74bb getStatistics handler, db-interface and ts API 2024-04-14 07:49:39 +02:00
Peter KW
564b2e6f58 Small fix 2024-04-13 21:36:02 +02:00
Peter KW
f66b6a0f0a Now uses new input types and checks if input is allowed + alerts and design changes 2024-04-13 21:18:01 +02:00
Peter KW
2f3f7261a0 Some new inputfield types 2024-04-13 21:14:17 +02:00
Peter KW
a5aac2065d Some constants for the different limits of password, username etc. 2024-04-13 21:13:26 +02:00
Peter KW
395e855257 Some regex to compare with strings 2024-04-13 21:12:46 +02:00
Peter KW
0e2b62f5ba Minor design and import changes 2024-04-13 21:10:37 +02:00
Peter KW
bcac9c020e Checks if new project name meets requirements 2024-04-13 21:09:07 +02:00
Peter KW
ebe0f380c5 Navigation button 2024-04-13 21:07:48 +02:00
Peter KW
6ca7f0d31f Improved alerts for error handling login 2024-04-13 21:07:32 +02:00
Peter KW
88c6757bd3 Checks if user already is selected role + minor design change 2024-04-13 21:07:08 +02:00
Peter KW
35a9c750d1 Routes back to home page on created project + design and alert updates 2024-04-13 20:53:05 +02:00
Peter KW
b71da1a698 Removed unused file + fixed paths 2024-04-13 20:50:23 +02:00
Davenludd
9ec7261a94 Merge branch 'dev' into gruppDM 2024-04-13 16:28:18 +02:00
Imbus
2d2b63938c Fix unsign 2024-04-13 09:36:34 +02:00
Peter KW
d9af77a8f7 Simplified + tiny design changes 2024-04-11 00:42:40 +02:00
Peter KW
43d83ca7b3 Search and design changes 2024-04-11 00:41:44 +02:00
Peter KW
2692127fdf Added some alerts and a new password field 2024-04-11 00:41:17 +02:00
Peter KW
1e0b32d32d Added search and some design changes 2024-04-11 00:39:33 +02:00
Peter KW
72d27bb644 Added search functionality and inputfield for changing p-name 2024-04-11 00:39:06 +02:00
Peter KW
385ceba248 Some "wrong login" alerts 2024-04-11 00:35:25 +02:00
Peter KW
0d4d77633d Prevents admin from changing its name 2024-04-11 00:34:56 +02:00
Peter KW
c8553d4eae Added search functionality and changed design a little 2024-04-11 00:34:27 +02:00
Peter KW
059a322d9e Changed inputfield slightly + update inputfields in files 2024-04-11 00:32:18 +02:00
Peter KW
7ec207bf03 Added more sample data 2024-04-11 00:29:06 +02:00
al8763be
57e969a562 Test for UnsignReport 2024-04-09 22:17:50 +02:00
Davenludd
ff07bd1ed6 Update OtherUsersTR component to include unsign and delete functionality 2024-04-09 22:14:00 +02:00
Davenludd
56e566ea9b Update confirmation message for deleting a user in ProjectMembers component 2024-04-09 20:57:22 +02:00
Davenludd
5e104ec7f9 Merge branch 'BumBranch' into gruppDM 2024-04-09 19:40:35 +02:00
al8763be
67723bfccc DeleteProject Handler + API function, ber till gud att denna funkar first try 2024-04-09 19:08:22 +02:00
Davenludd
1631c8b6b0 Merge branch 'dev' into gruppDM 2024-04-09 18:18:02 +02:00
al8763be
a5e3d4259d unsignReport handler + API function 2024-04-09 17:39:10 +02:00
al8763be
f57c445ead Merge remote-tracking branch 'origin/dev' into BumBranch 2024-04-09 16:11:53 +02:00
al8763be
11341ce37e Merge branch 'master' into BumBranch 2024-04-09 16:11:30 +02:00
Davenludd
a266a6f7fc Update ProjectMembers component to allow deleting users from a project 2024-04-09 10:59:27 +02:00
Davenludd
b56e4ed76e Add Button component to OtherUsersTR and update route path in main.tsx to include signedOrUnsigned parameter 2024-04-09 10:15:19 +02:00
Davenludd
f61449dea1 Update AllTimeReportsInProjectOtherUser component to include signed status in route path 2024-04-09 10:14:48 +02:00
Davenludd
fd7c609e5d Update route path in main.tsx to include signedOrUnsigned parameter 2024-04-09 10:14:34 +02:00
Davenludd
9725a0fc48 Update EditWeeklyReport component to prevent editing of signed reports 2024-04-08 22:57:34 +02:00
Davenludd
6d0775586e Update EditWeeklyReport component to change depending on if the report is signed or not 2024-04-08 22:44:55 +02:00
Davenludd
badeb84282 Update route path in main.tsx to include signedOrUnsigned parameter 2024-04-08 22:43:46 +02:00
Davenludd
e63377effa Update route path in main.tsx to include signedOrUnsigned parameter 2024-04-08 22:43:26 +02:00
Imbus
1fd4cd18e3 Version bumps in frontend 2024-04-05 00:03:49 +02:00
Imbus
9d65764899 Go version bumps 2024-04-04 23:56:35 +02:00
Imbus
a81020f660 Removing verbose flag from testing 2024-04-04 23:26:53 +02:00
Imbus
7e88bd69c1 Killing process if tests fail in itest target 2024-04-04 23:24:38 +02:00
Imbus
d65bbc897d Fixing logic error in paths related to getting reports 2024-04-04 23:23:11 +02:00
Davenludd
7964e0d1d7 Merge branch 'gruppDM' into dev 2024-04-04 23:04:38 +02:00
Davenludd
591d0201cf Update AllTimeReportsInProjectOtherUser component to display signed status correct 2024-04-04 23:03:30 +02:00
Imbus
f7f29241b3 Fixing itest makefile target 2024-04-04 23:02:19 +02:00
Imbus
1f4abc2a6a Linter fixes and proper error handling in PromoteToPm 2024-04-04 23:02:10 +02:00
Davenludd
7635791f09 Merge branch 'frontend' into gruppDM 2024-04-04 22:40:59 +02:00
Imbus
68e6f94d13 Fixing report signing database interface 2024-04-04 22:39:55 +02:00
Davenludd
91bccba871 Add username parameter and error alerts in GetOtherUsersReport component 2024-04-04 22:20:09 +02:00
Davenludd
a6f3fc4a1c Fixed DisplayUserProject component to fetch and display usernames 2024-04-04 22:19:35 +02:00
Davenludd
544383809b Merge branch 'frontend' into gruppDM 2024-04-04 20:10:57 +02:00
Imbus
abfb79b991 getUserName path implemented 2024-04-04 19:54:31 +02:00
Peter KW
db6fdf3c29 Small design fix 2024-04-04 12:27:38 +02:00
Peter KW
5bc7aadfa3 Merge remote-tracking branch 'origin/frontend' into gruppPP 2024-04-04 12:22:55 +02:00
Peter KW
2d8d200340 Can now remove users from projects 2024-04-04 12:22:28 +02:00
Peter KW
0cb6af03e4 Merge remote-tracking branch 'origin/dev' into gruppPP 2024-04-04 12:00:55 +02:00
Davenludd
f75e6e4a6e Update useEffect dependencies in AllTimeReportsInProjectOtherUser component 2024-04-04 11:55:04 +02:00
Imbus
10d06767c4 Fix for javascript optional parameter formatting error 2024-04-04 11:55:04 +02:00
Imbus
ee49bbde69 Docs 2024-04-04 11:55:04 +02:00
Davenludd
266aaa482c Fix initial state values in TimePerRole component 2024-04-04 11:55:04 +02:00
Davenludd
194e1d52a8 Fix input fields to be read-only in OtherUsersTR and initialize state variables in TimePerRole 2024-04-04 11:55:04 +02:00
Davenludd
82e72432f6 Update API method to get all weekly reports 2024-04-04 11:55:04 +02:00
Imbus
620073f688 Logic error in getAllWeeklyReports fixed 2024-04-04 11:55:04 +02:00
Imbus
7313a27b31 Rename 2024-04-04 11:55:04 +02:00
Davenludd
d7789ab844 Refactor API call in AllTimeReportsInProjectOtherUser component 2024-04-04 11:55:04 +02:00
Imbus
4319f30799 Tests for getAllWeeklyReports 2024-04-04 11:55:03 +02:00
Imbus
2ca8c60418 TS Api for getAllWeeklyReport 2024-04-04 11:55:03 +02:00
Imbus
0a951ecd2b Rename, fix and testing for getAllWeeklyReports path 2024-04-04 11:55:03 +02:00
Peter KW
13eb6597a7 Some fixes to how they handle names and inputs 2024-04-04 11:54:34 +02:00
Peter KW
9a0f855d2b Fixes to adding members 2024-04-04 11:26:39 +02:00
Davenludd
7c995573ef Update error message in NewWeeklyReport component 2024-04-04 11:19:35 +02:00
Mattias
a5ea74c996 Add useNavigate hook and handle navigation based on user "role" 2024-04-04 10:29:25 +02:00
Davenludd
a0ff329845 Update useEffect dependencies in AllTimeReportsInProjectOtherUser component 2024-04-03 18:36:17 +02:00
Imbus
e67c54540c RemoveUserFromProject handler implemented, corresponding TS api, untested 2024-04-03 18:08:02 +02:00
Davenludd
91671b2b63 Merge branch 'frontend' into gruppDM 2024-04-03 18:07:18 +02:00
Imbus
76ae587116 Fix for javascript optional parameter formatting error 2024-04-03 18:05:56 +02:00
Davenludd
66a064c9f8 Fix initial state values in TimePerRole component 2024-04-03 18:05:30 +02:00
Davenludd
be963b2281 Fix input fields to be read-only in OtherUsersTR and initialize state variables in TimePerRole 2024-04-03 18:05:19 +02:00
Davenludd
c0d36f8df1 Update API method to get all weekly reports 2024-04-03 18:05:07 +02:00
Imbus
f9c30c4951 Docs 2024-04-03 17:48:02 +02:00
Davenludd
520e2f74f0 Merge branch 'frontend' into gruppDM 2024-04-03 17:45:32 +02:00
Imbus
12810075f9 Logic error in getAllWeeklyReports fixed 2024-04-03 17:44:50 +02:00
Davenludd
8cc0458e1b Refactor API call in AllTimeReportsInProjectOtherUser component 2024-04-03 17:44:26 +02:00
Imbus
12a2691d55 Rename 2024-04-03 17:40:48 +02:00
Imbus
9c5aa10414 Tests for getAllWeeklyReports 2024-04-03 17:32:50 +02:00
Imbus
fcd035fe6e TS Api for getAllWeeklyReport 2024-04-03 17:32:07 +02:00
Imbus
a39cfedad3 Rename, fix and testing for getAllWeeklyReports path 2024-04-03 17:31:39 +02:00
Imbus
a1d2520d88 Updating typescript api 2024-04-03 17:12:29 +02:00
Imbus
903132e56d Polishing tests 2024-04-03 17:06:08 +02:00
Imbus
7c973009ac Adding pycache to gitignore 2024-04-03 15:54:27 +02:00
Imbus
ffe5d53625 Splitting test script 2024-04-03 15:53:52 +02:00
Imbus
8ea6dec346 Fixes for various paths 2024-04-03 15:53:36 +02:00
Imbus
61a2d1ce0c PromoteToPm handler 2024-04-03 15:53:15 +02:00
Imbus
9cca8edd9d Additional tests 2024-04-03 14:50:04 +02:00
Imbus
1f9ccca9bf Fixing integration test script 2024-04-03 14:21:55 +02:00
Davenludd
6b880244a1 Merge branch 'gruppDM' into frontend 2024-04-03 14:00:14 +02:00
Davenludd
4d7b3e0d57 Refactor API call and types in AllTimeReportsInProjectOtherUser component 2024-04-03 13:59:44 +02:00
Peter KW
46eb3c76a8 Small fix to statistics 2024-04-02 19:18:05 +02:00
Peter KW
632676e3d2 Added admin to all projects in sample_data 2024-04-02 19:16:02 +02:00
Mattias
1b4e521508 Update variable names in TimePerActivity component 2024-04-02 18:02:40 +02:00
Mattias
a7cc48d392 Refactor TimePerRole component to use API response for time per activity 2024-04-02 18:02:04 +02:00
Davenludd
ff37236cf6 Minor fixes TimePerActivity component to use readOnly input fields 2024-04-02 17:35:02 +02:00
Davenludd
eb741ba20d Add total time calculation to DisplayUnsignedReports component 2024-04-02 17:33:20 +02:00
Davenludd
00ca5514e5 Added sign functionality to component 2024-04-02 17:27:17 +02:00
Mattias
7c7755085e Add comments for getUnsignedReportsInProject in API 2024-04-02 17:11:30 +02:00
Mattias
1e1677fc57 Refactor getUnsignedReports in DisplayUnsignedReports component 2024-04-02 17:07:47 +02:00
Mattias
0b8b430f38 Refactor DisplayUnsignedReports component to use API and WeeklyReport type 2024-04-02 17:05:55 +02:00
Mattias
93659a72dc Add getUnsignedReportsInProject API method 2024-04-02 17:05:46 +02:00
Mattias
8d6da684bf Add username retrieval from local storage 2024-04-02 16:21:23 +02:00
Davenludd
762a1b7368 Merge branch 'frontend' into gruppDM 2024-04-02 15:51:11 +02:00
Mattias
398305d3ed Fix input validation in NewWeeklyReport component 2024-04-02 15:43:18 +02:00
Mattias
6c2213b488 Update handleUpdateWeeklyReport function and fix input validation 2024-04-02 15:43:12 +02:00
Mattias
b3e363f391 Add updateWeeklyReport function to API.ts 2024-04-02 15:43:05 +02:00
Peter KW
51a4d2a0b7 Fix to comments 2024-04-02 13:45:03 +02:00
Peter KW
524bd6c691 Can now delete project 2024-04-02 13:44:31 +02:00
Peter KW
b50d88f670 DeleteProject component 2024-04-02 13:44:06 +02:00
Peter KW
3ed4393c77 Fixed fetch path in removeProject 2024-04-02 13:43:32 +02:00
Peter KW
75876e43da Minor design fix 2024-04-02 13:22:46 +02:00
Peter KW
ea5bbf5f0a Merge branch 'frontend' into gruppPP 2024-04-02 13:18:25 +02:00
Peter KW
948dcce1ca Some fixes to design and comments 2024-04-02 13:14:08 +02:00
Peter KW
cb68a6323b Now shows project statistics 2024-04-02 13:11:33 +02:00
Peter KW
ca88daf493 GetProjectTimes component 2024-04-02 13:10:04 +02:00
Peter KW
644d0ee12c getProjectTimes API 2024-04-02 13:09:28 +02:00
Peter KW
6efc961774 Added getProjectTimes 2024-04-02 13:08:55 +02:00
Davenludd
7df1654bdc Merge branch 'frontend' into gruppDM 2024-04-02 11:49:46 +02:00
Davenludd
6dfa917cf0 Fix in DisplayUserProjects component 2024-04-02 11:37:53 +02:00
Peter KW
17a571fd7c Uses component to get projects now 2024-04-01 02:25:12 +02:00
Peter KW
f3466854c7 Removed unused pages and paths to them in main 2024-04-01 02:24:26 +02:00
Peter KW
1212b3c5ef Removed some stuff 2024-04-01 02:20:48 +02:00
Peter KW
dc98fb510e Clears username+password fields on successful register 2024-04-01 02:17:57 +02:00
Peter KW
58deef400a Removed unused code 2024-04-01 02:17:02 +02:00
Peter KW
cc039d27ae New modal for member info 2024-04-01 02:16:23 +02:00
Peter KW
3981190c7a Can now change username in this modal + moved some stuff to a separate modal 2024-04-01 02:16:06 +02:00
Peter KW
9b0a231701 Some fixes to ChangeUsername 2024-04-01 02:14:44 +02:00
Peter KW
e06aced6dd ChangeRole component to change role and a view for it 2024-04-01 02:13:24 +02:00
Peter KW
68fbbb4b19 Added some alerts + removed unused code 2024-04-01 02:09:28 +02:00
Peter KW
378dd99592 Changed so that you can only change other users role 2024-04-01 02:08:19 +02:00
Peter KW
6fa8135e32 ChangeUserRole API added + bugfix 2024-04-01 02:02:22 +02:00
Peter KW
60fb333090 Fix to paths 2024-03-31 21:04:58 +02:00
Peter KW
e7911574be Removed unused files 2024-03-31 20:56:40 +02:00
Peter KW
0c8a394f74 Small fix so that it uses component for getting users in a proj 2024-03-31 20:56:08 +02:00
Peter KW
5f42fa7818 Fixed types and imports of types 2024-03-31 20:54:00 +02:00
Peter KW
8b6462abee Changed so that username is required to get projects, so that you can get another user's projects (for admin stuff) 2024-03-29 20:19:22 +01:00
Peter KW
4ab23b3c3c Merge branch 'dev' into gruppPP 2024-03-29 20:05:10 +01:00
Imbus
bc9b01d85a Example component GenApiDemo on how to use the new API 2024-03-29 19:37:21 +01:00
Imbus
d2b4bf2a89 Brand new typescript API interface generated from swagger 2024-03-29 19:36:47 +01:00
Imbus
8d5329146d New tygo generated goTypes 2024-03-29 19:36:06 +01:00
Imbus
77f028fd39 Prettierignore for generated files 2024-03-29 19:23:35 +01:00
Imbus
f1e15137d6 Freshly generated swagger docs 2024-03-29 18:43:26 +01:00
Imbus
87a19bfd4e Swagger annotations for JWT key 2024-03-29 18:42:53 +01:00
Imbus
c2fa9aa0c1 Lots of fiddling with swagger annotations in user related handlers 2024-03-29 18:42:12 +01:00
Imbus
1385011769 Make swagger-typescript-api makefile target wipe the previous version 2024-03-29 18:41:20 +01:00
Davenludd
c1f49915ba Refactor signReport method signature 2024-03-29 18:29:38 +01:00
Davenludd
2aaa327a01 Merge branch 'dev' into gruppDM 2024-03-29 17:55:32 +01:00
Davenludd
05545f6f88 Minor fixes 2024-03-29 17:53:37 +01:00
Imbus
374e357820 Add database.txt to clean target 2024-03-29 16:41:55 +01:00
al8763be
bcb661dc22 bug fix getUnsignedReports 2024-03-29 13:02:31 +01:00
Peter KW
b036ef906c Small fixes in all files that fetches user projects, so that they pass username as argument to GetProjects-function 2024-03-28 21:31:30 +01:00
Peter KW
85795f5406 Changed GetUserProjects so that you have to get username from params. Now admin can choose a user and see what projects the user belongs to 2024-03-28 21:25:59 +01:00
100 changed files with 5710 additions and 2400 deletions

1
.gitignore vendored
View file

@ -14,6 +14,7 @@ diagram.puml
backend/*.png
backend/*.jpg
backend/*.svg
__pycache__
/go.work.sum
/package-lock.json

View file

@ -34,6 +34,7 @@ clean:
rm -f plantuml.jar
rm -f erd.png
rm -f config.toml
rm -f database.txt
# Test target
test: db.sqlite3
@ -46,7 +47,7 @@ itest:
make build
./bin/$(PROC_NAME) >/dev/null 2>&1 &
sleep 1 # Adjust if needed
python ../testing.py
python ../testing/testing.py || pkill $(PROC_NAME)
pkill $(PROC_NAME)
# Get dependencies target
@ -105,6 +106,7 @@ docs:
swag init -outputTypes go
api: ./docs/swagger.json
rm ../frontend/src/API/GenApi.ts
npx swagger-typescript-api \
--api-class-name GenApi \
--path ./docs/swagger.json \

View file

@ -21,21 +21,21 @@ const docTemplate = `{
"paths": {
"/login": {
"post": {
"description": "logs the user in and returns a jwt token",
"description": "Logs in a user and returns a JWT token",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
"application/json"
],
"tags": [
"User"
"Auth"
],
"summary": "login",
"summary": "Login",
"parameters": [
{
"description": "login info",
"name": "NewUser",
"description": "User credentials",
"name": "body",
"in": "body",
"required": true,
"schema": {
@ -45,9 +45,9 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "Successfully signed token for user",
"description": "JWT token",
"schema": {
"type": "Token"
"$ref": "#/definitions/types.Token"
}
},
"400": {
@ -71,29 +71,26 @@ const docTemplate = `{
}
}
},
"/loginerenew": {
"/loginrenew": {
"post": {
"security": [
{
"bererToken": []
"JWT": []
}
],
"description": "renews the users token",
"consumes": [
"description": "Renews the users token.",
"produces": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"User"
"Auth"
],
"summary": "LoginRenews",
"responses": {
"200": {
"description": "Successfully signed token for user",
"schema": {
"type": "Token"
"$ref": "#/definitions/types.Token"
}
},
"401": {
@ -111,9 +108,64 @@ const docTemplate = `{
}
}
},
"/promote/{projectName}": {
"put": {
"security": [
{
"JWT": []
}
],
"description": "Promote a user to project manager",
"consumes": [
"text/plain"
],
"produces": [
"text/plain"
],
"tags": [
"Auth"
],
"summary": "Promote to project manager",
"parameters": [
{
"type": "string",
"description": "Project name",
"name": "projectName",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User name",
"name": "userName",
"in": "query",
"required": true
}
],
"responses": {
"403": {
"description": "Forbidden",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/promoteToAdmin": {
"post": {
"description": "promote chosen user to admin",
"security": [
{
"JWT": []
}
],
"description": "Promote chosen user to site admin",
"consumes": [
"application/json"
],
@ -139,7 +191,7 @@ const docTemplate = `{
"200": {
"description": "Successfully promoted user",
"schema": {
"type": "json"
"$ref": "#/definitions/types.Token"
}
},
"400": {
@ -173,7 +225,7 @@ const docTemplate = `{
"text/plain"
],
"tags": [
"User"
"Auth"
],
"summary": "Register",
"parameters": [
@ -211,6 +263,11 @@ const docTemplate = `{
},
"/userdelete/{username}": {
"delete": {
"security": [
{
"JWT": []
}
],
"description": "UserDelete deletes a user from the database",
"consumes": [
"application/json"
@ -252,12 +309,14 @@ const docTemplate = `{
},
"/users/all": {
"get": {
"description": "lists all users",
"consumes": [
"application/json"
"security": [
{
"JWT": []
}
],
"description": "lists all users",
"produces": [
"text/plain"
"application/json"
],
"tags": [
"User"
@ -265,9 +324,12 @@ const docTemplate = `{
"summary": "ListsAllUsers",
"responses": {
"200": {
"description": "Successfully signed token for user",
"description": "Successfully returned all users",
"schema": {
"type": "json"
"type": "array",
"items": {
"type": "string"
}
}
},
"401": {
@ -291,16 +353,27 @@ const docTemplate = `{
"type": "object",
"properties": {
"password": {
"type": "string"
"type": "string",
"example": "password123"
},
"username": {
"type": "string",
"example": "username123"
}
}
},
"types.Token": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"bererToken": {
"JWT": {
"description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**",
"type": "apiKey",
"name": "Authorization",
"in": "header"

View file

@ -13,10 +13,10 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-openapi/jsonpointer v0.20.3 // indirect
github.com/go-openapi/jsonreference v0.20.5 // indirect
github.com/go-openapi/spec v0.20.15 // indirect
github.com/go-openapi/swag v0.22.10 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@ -25,10 +25,10 @@ require (
github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/tools v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
modernc.org/libc v1.49.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
@ -42,7 +42,7 @@ require (
// These are all for fiber
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/gofiber/fiber/v2 v2.52.2
github.com/gofiber/fiber/v2 v2.52.4
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@ -52,5 +52,5 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sys v0.19.0 // indirect
)

View file

@ -10,20 +10,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-openapi/jsonpointer v0.20.3 h1:jykzYWS/kyGtsHfRt6aV8JTB9pcQAXPIA7qlZ5aRlyk=
github.com/go-openapi/jsonpointer v0.20.3/go.mod h1:c7l0rjoouAuIxCm8v/JWKRgMjDG/+/7UBWsXMrv6PsM=
github.com/go-openapi/jsonreference v0.20.5 h1:hutI+cQI+HbSQaIGSfsBsYI0pHk+CATf8Fk5gCSj0yI=
github.com/go-openapi/jsonreference v0.20.5/go.mod h1:thAqAp31UABtI+FQGKAQfmv7DbFpKNUlva2UPCxKu2Y=
github.com/go-openapi/spec v0.20.15 h1:8bDcVxF607pTh9NpPwgsH4J5Uhh5mV5XoWnkurdiY+U=
github.com/go-openapi/spec v0.20.15/go.mod h1:o0upgqg5uYFG7O5mADrDVmSG3Wa6y6OLhwiCqQ+sTv4=
github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA=
github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY=
github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc=
github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
@ -85,8 +85,8 @@ golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -94,16 +94,26 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.15.0 h1:uwfCZOkKhaNNotgYW7kxkJwrkQC1HfGitt/7ousudJE=
modernc.org/ccgo/v4 v4.15.0/go.mod h1:XVITcYGiI+O97UNDLMsnZ9ZjJOhC+ACX+TfxpsWWyRc=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.49.1 h1:r4UaWllkYXRPA7Mq/KzmassZBvNJiH9egF4O/KV/gdE=
modernc.org/libc v1.49.1/go.mod h1:Hx2rWfza47GSzCluTU7Vf0Qx3z9rWCVORL6RNgq+Xog=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=

View file

@ -2,11 +2,12 @@ package database
import (
"embed"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
@ -17,12 +18,11 @@ type Database interface {
AddUser(username string, password string) error
CheckUser(username string, password string) bool
RemoveUser(username string) error
RemoveUserFromProject(username string, projectname string) error
PromoteToAdmin(username string) error
GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error
DeleteProject(name 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
@ -35,20 +35,26 @@ type Database interface {
GetProject(projectId int) (types.Project, error)
GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error)
GetAllWeeklyReports(username string, projectname string) ([]types.WeeklyReportList, error)
GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username 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)
UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
RemoveProject(projectname string) error
GetUserName(id int) (string, error)
UnsignWeeklyReport(reportId int, projectManagerId int) error
DeleteReport(reportID int) error
ChangeProjectName(projectName string, newProjectName string) error
ChangeUserPassword(username string, password string) error
}
// This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct {
*sqlx.DB
*sqlx.Tx
}
type UserProjectMember struct {
@ -86,8 +92,23 @@ const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles
JOIN projects ON user_roles.project_id = projects.id
WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'`
const removeUserFromProjectQuery = `DELETE FROM user_roles
WHERE user_id = (SELECT id FROM users WHERE username = ?)
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
func DbConnect(dbpath string) Database {
func DbConnect(dbpath string) sqlx.DB {
// Open the database
db, err := sqlx.Connect("sqlite", dbpath)
if err != nil {
@ -100,7 +121,25 @@ func DbConnect(dbpath string) Database {
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 {
@ -147,6 +186,11 @@ func (d *Db) AddUserToProject(username string, projectname string, role string)
return err
}
func (d *Db) RemoveUserFromProject(username string, projectname string) error {
_, err := d.Exec(removeUserFromProjectQuery, username, projectname)
return err
}
// ChangeUserRole changes the role of a user within a project.
func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
// Execute the SQL query to change the user's role
@ -199,25 +243,15 @@ 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 {
tx := d.MustBegin()
// 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 := tx.Rollback(); err != nil {
return err
}
return err
}
// 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 := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
@ -225,16 +259,7 @@ func (d *Db) AddProject(name string, description string, username string) error
}
func (d *Db) DeleteProject(projectID string, username string) error {
tx := d.MustBegin()
_, 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)
}
_, err := d.Exec(deleteProject, projectID, username)
return err
}
@ -339,9 +364,14 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err
}
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId)
if err != nil {
return err
}
@ -356,6 +386,36 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err
}
func (d *Db) UnsignWeeklyReport(reportId int, projectManagerId int) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
if err != nil {
return err
}
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId)
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors.New("project manager doesn't have permission to unsign the report")
}
// Update the signed_by field of the specified report
_, err = d.Exec("UPDATE weekly_reports SET signed_by = NULL WHERE report_id = ?;", reportId)
return err
}
func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) {
// Define the SQL query to fetch unsigned reports for a given user
query := `
@ -423,7 +483,7 @@ func (d *Db) IsSiteAdmin(username string) (bool, error) {
// 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 {
func Migrate(db sqlx.DB) error {
// Read the embedded scripts directory
files, err := scripts.ReadDir("migrations")
if err != nil {
@ -435,7 +495,7 @@ func (d *Db) Migrate() error {
return nil
}
tr := d.MustBegin()
tr := db.MustBegin()
// Iterate over each SQL file and execute it
for _, file := range files {
@ -463,8 +523,8 @@ func (d *Db) Migrate() error {
return nil
}
// GetWeeklyReportsUser retrieves weekly reports for a specific user and project.
func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) {
// GetAllWeeklyReports retrieves weekly reports for a specific user and project.
func (d *Db) GetAllWeeklyReports(username string, projectName string) ([]types.WeeklyReportList, error) {
query := `
SELECT
wr.week,
@ -521,7 +581,7 @@ func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, d
}
// MigrateSampleData applies sample data to the database.
func (d *Db) MigrateSampleData() error {
func MigrateSampleData(db sqlx.DB) error {
// Insert sample data
files, err := sampleData.ReadDir("sample_data")
if err != nil {
@ -531,7 +591,7 @@ func (d *Db) MigrateSampleData() error {
if len(files) == 0 {
println("No sample data files found")
}
tr := d.MustBegin()
tr := db.MustBegin()
// Iterate over each SQL file and execute it
for _, file := range files {
@ -568,7 +628,7 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
WHERE projects.name = ?
`
rows, err := d.DB.Query(query, projectName)
rows, err := d.Query(query, projectName)
if err != nil {
return nil, err
}
@ -601,3 +661,25 @@ func (d *Db) RemoveProject(projectname string) error {
_, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname)
return err
}
func (d *Db) GetUserName(id int) (string, error) {
var username string
err := d.Get(&username, "SELECT username FROM users WHERE id = ?", id)
return username, err
}
func (d *Db) DeleteReport(reportID int) error {
_, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID)
return err
}
// ChangeProjectName is a handler that changes the name of a project
func (d *Db) ChangeProjectName(projectName string, newProjectName string) error {
_, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName)
return err
}
func (d *Db) ChangeUserPassword(username string, password string) error {
_, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username)
return err
}

View file

@ -9,11 +9,13 @@ import (
// setupState initializes a database instance with necessary setup for testing
func setupState() (Database, error) {
db := DbConnect(":memory:")
err := db.Migrate()
err := Migrate(db)
if err != nil {
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.
@ -583,6 +585,94 @@ func TestSignWeeklyReport(t *testing.T) {
}
}
func TestUnsignWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add both regular users as members to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
err = db.AddUserToProject("projectManager", "testproject", "project_manager")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for one of the regular users
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Print project manager's ID
projectManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
// Sign the report with the project manager
err = db.SignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("SignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's signed
signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is signed by the project manager
if *signedReport.SignedBy != projectManagerID {
t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy)
}
// Unsign the report
err = db.UnsignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("UnsignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's unsigned
unsignedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is unsigned
if unsignedReport.SignedBy != nil {
t.Error("Expected SignedBy to be nil, got", unsignedReport.SignedBy)
}
}
// TestSignWeeklyReportByAnotherProjectManager tests the scenario where a project manager attempts to sign a weekly report for a user who is not assigned to their project
func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
db, err := setupState()
@ -705,7 +795,7 @@ func TestGetWeeklyReportsUser(t *testing.T) {
t.Error("AddWeeklyReport failed:", err)
}
reports, err := db.GetWeeklyReportsUser("testuser", "testproject")
reports, err := db.GetAllWeeklyReports("testuser", "testproject")
if err != nil {
t.Error("GetWeeklyReportsUser failed:", err)
}
@ -965,3 +1055,90 @@ func TestRemoveProject(t *testing.T) {
}
func TestDeleteReport(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
// Promote user to Admin
err = db.PromoteToAdmin("demouser")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
// create a weekly report
err = db.AddWeeklyReport("projecttest", "demouser", 16, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Check if the report was added
report, err := db.GetWeeklyReport("demouser", "projecttest", 16)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Remove report
err = db.DeleteReport(report.ReportId)
if err != nil {
t.Error("RemoveReport failed:", err)
}
// Check if the report was removed
report, err = db.GetWeeklyReport("demouser", "projecttest", 16)
if err == nil {
t.Error("RemoveReport failed: report not removed")
}
}
func TestChangeProjectName(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
// Promote user to Admin
err = db.PromoteToAdmin("demouser")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
// Change project name
err = db.ChangeProjectName("projecttest", "newprojectname")
if err != nil {
t.Error("ChangeProjectName failed:", err)
}
// Check if the project name was changed
projects, err := db.GetAllProjects()
if err != nil {
t.Error("GetAllProjects failed:", err)
}
if projects[0].Name != "newprojectname" {
t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name)
}
}
func TestChangeUserPassword(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
_ = db.AddUser("testuser", "password")
// Change user password
err = db.ChangeUserPassword("testuser", "newpassword")
if err != nil {
t.Error("ChangeUserPassword failed:", err)
}
// Check if the password was changed
if !db.CheckUser("testuser", "newpassword") {
t.Error("ChangeUserPassword failed: password not changed")
}
}

View file

@ -1,11 +1,28 @@
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"
func DbMiddleware(db *Database) func(c *fiber.Ctx) error {
// Simple middleware that provides a transaction as a local key "db"
func DbMiddleware(db *sqlx.DB) 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()
}
}

View file

@ -1,52 +1,220 @@
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)
VALUES ("user", "123");
INSERT OR IGNORE INTO projects(name, description, owner_user_id)
VALUES ("projecttest1", "Description for projecttest1", 1),
("projecttest2", "Description for projecttest2", 1),
("projecttest3", "Description for projecttest3", 1),
("projecttest4", "Description for projecttest4", 1),
("projecttest5", "Description for projecttest5", 1),
("projecttest6", "Description for projecttest6", 1),
("projecttest7", "Description for projecttest7", 1),
("projecttest8", "Description for projecttest8", 1),
("projecttest9", "Description for projecttest9", 1),
("projecttest10", "Description for projecttest10", 1),
("projecttest11", "Description for projecttest11", 1),
("projecttest12", "Description for projecttest12", 1),
("projecttest13", "Description for projecttest13", 1),
("projecttest14", "Description for projecttest14", 1),
("projecttest15", "Description for projecttest15", 1);
INSERT OR IGNORE INTO users(username, password)
VALUES ("user2", "123");
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,1,"project_manager"),
(1,2,"project_manager"),
(1,3,"project_manager"),
(1,4,"project_manager"),
(1,5,"project_manager"),
(1,6,"project_manager"),
(1,7,"project_manager"),
(1,8,"project_manager"),
(1,9,"project_manager"),
(1,10,"project_manager"),
(1,11,"project_manager"),
(1,12,"project_manager"),
(1,13,"project_manager"),
(1,14,"project_manager"),
(1,15,"project_manager"),
(2,1,"project_manager"),
(2,2,"member"),
(2,3,"member"),
(2,4,"member"),
(2,5,"member"),
(2,6,"member"),
(2,7,"member"),
(2,8,"member"),
(2,9,"member"),
(2,10,"member"),
(2,11,"member"),
(2,12,"member"),
(2,13,"member"),
(2,14,"member"),
(2,15,"member"),
(3,1,"member"),
(3,2,"member"),
(3,3,"member"),
(3,4,"member"),
(3,5,"member"),
(3,6,"member"),
(3,7,"member"),
(3,8,"member"),
(3,9,"member"),
(3,10,"member"),
(3,11,"member"),
(3,12,"member"),
(3,13,"member"),
(3,14,"member"),
(3,15,"member"),
(4,1,"member"),
(4,2,"member"),
(4,3,"member"),
(4,4,"member"),
(4,5,"member"),
(4,6,"member"),
(4,7,"member"),
(4,8,"member"),
(4,9,"member"),
(4,10,"member"),
(4,11,"member"),
(4,12,"member"),
(4,13,"member"),
(4,14,"member"),
(4,15,"member"),
(5,1,"member"),
(5,2,"member"),
(5,3,"member"),
(5,4,"member"),
(5,5,"member"),
(5,6,"member"),
(5,7,"member"),
(5,8,"member"),
(5,9,"member"),
(5,10,"member"),
(5,11,"member"),
(5,12,"member"),
(5,13,"member"),
(5,14,"member"),
(5,15,"member");
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (2, 1, 12, 100, 50, 30, 150, 80, 20, NULL),
(3, 1, 12, 200, 80, 20, 200, 100, 30, NULL),
(3, 1, 14, 150, 70, 40, 180, 90, 25, NULL),
(3, 2, 12, 120, 60, 35, 160, 85, 15, NULL),
(3, 3, 12, 180, 90, 25, 190, 110, 40, NULL),
(2, 1, 13, 130, 70, 40, 170, 95, 35, NULL),
(3, 1, 15, 140, 60, 50, 200, 120, 30, NULL),
(2, 2, 11, 110, 50, 45, 140, 70, 25, NULL),
(3, 3, 14, 170, 80, 30, 180, 100, 35, NULL),
(3, 3, 15, 200, 100, 20, 220, 130, 45, NULL),
(2, 4, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 5, 14, 150, 70, 30, 180, 90, 25, NULL),
(3, 5, 15, 180, 90, 20, 190, 110, 35, NULL),
(2, 6, 11, 100, 50, 35, 130, 60, 20, NULL),
(3, 7, 14, 170, 80, 25, 180, 100, 30, NULL),
(2, 8, 12, 130, 70, 30, 170, 90, 25, NULL),
(2, 8, 13, 150, 80, 20, 180, 110, 35, NULL),
(3, 9, 12, 140, 60, 40, 180, 100, 30, NULL),
(3, 10, 11, 120, 50, 45, 150, 70, 25, NULL),
(2, 11, 13, 110, 60, 35, 140, 80, 30, NULL),
(3, 12, 12, 160, 70, 30, 180, 100, 35, NULL),
(3, 12, 13, 180, 90, 25, 190, 110, 40, NULL),
(3, 12, 14, 200, 100, 20, 220, 130, 45, NULL),
(2, 13, 11, 100, 50, 45, 130, 60, 20, NULL),
(2, 13, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 14, 13, 140, 70, 30, 160, 90, 35, NULL),
(3, 15, 12, 150, 80, 25, 180, 100, 30, NULL),
(3, 15, 13, 170, 90, 20, 190, 110, 35, NULL);
INSERT OR IGNORE INTO site_admin VALUES (1);
INSERT OR IGNORE INTO 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");
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (2, 1, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 1, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 1, 14, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 2, 12, 20, 10, 5, 30, 15, 10, NULL);
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (3, 3, 12, 20, 10, 5, 30, 15, 10, NULL);

View file

@ -10,42 +10,33 @@ import (
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func 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)
pm_name := claims["name"].(string)
isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername)
project := c.Params("projectName")
username := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking admin status:", err)
log.Info("Error checking if user is project manager:", 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")
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Add the user to the project with the specified role
err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role)
err = db.GetDb(c).AddUserToProject(username, project, "member")
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)
log.Info("User : ", username, " added to project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -0,0 +1,43 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ChangeProjectName is a handler that changes the name of a project
func ChangeProjectName(c *fiber.Ctx) error {
//check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract the necessary parameters from the request
projectName := c.Params("projectName")
newProjectName := c.Query("newProjectName")
// Check if user is site admin
issiteadmin, err := db.GetDb(c).IsSiteAdmin(username)
if err != nil {
log.Warn("Error checking if siteadmin:", err)
return c.Status(500).SendString(err.Error())
} else if !issiteadmin {
log.Warn("User is not siteadmin")
return c.Status(401).SendString("User is not siteadmin")
}
// Perform the project name change
err = db.GetDb(c).ChangeProjectName(projectName, newProjectName)
if err != nil {
log.Warn("Error changing project name:", err)
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.Status(200).SendString("Project name changed successfully")
}

View file

@ -4,15 +4,16 @@ import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/gofiber/fiber/v2/log"
)
// GetUserProjects returns all projects that the user is a member of
func GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
username := c.Params("username")
if username == "" {
log.Info("No username provided")
return c.Status(400).SendString("No username provided")
}
// Then dip into the database to get the projects
projects, err := db.GetDb(c).GetProjectsForUser(username)

View file

@ -24,7 +24,13 @@ func ProjectRoleChange(c *fiber.Ctx) error {
return c.Status(400).SendString(err.Error())
}
log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role)
// Check if user is trying to change its own role
if username == data.UserName {
log.Info("Can't change your own role")
return c.Status(403).SendString("Can't change your own role")
}
log.Info("Changing role for user: ", data.UserName, " in project: ", data.Projectname, " to: ", data.Role)
// Dubble diping and checcking if current user is
if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil {
@ -36,7 +42,7 @@ func ProjectRoleChange(c *fiber.Ctx) error {
}
// Change the user's role within the project in the database
if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil {
if err := db.GetDb(c).ChangeUserRole(data.UserName, data.Projectname, data.Role); err != nil {
return c.Status(500).SendString(err.Error())
}

View file

@ -0,0 +1,55 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// @Summary Promote to project manager
// @Description Promote a user to project manager
// @Tags Auth
// @Security JWT
// @Accept plain
// @Produce plain
// @Param projectName path string true "Project name"
// @Param userName query string true "User name"
// @Failure 500 {string} string "Internal server error"
// @Failure 403 {string} string "Forbidden"
// @Router /promote/{projectName} [put]
//
// Login logs in a user and returns a JWT token
// Promote to project manager
func PromoteToPm(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
pm_name := claims["name"].(string)
project := c.Params("projectName")
new_pm_name := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Add the user to the project with the specified role
err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager")
if err != nil {
log.Info("Error promoting user to project manager:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -0,0 +1,40 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func RemoveUserFromProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
pm_name := claims["name"].(string)
project := c.Params("projectName")
username := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Remove the user from the project
if err = db.GetDb(c).RemoveUserFromProject(username, project); err != nil {
log.Info("Error removing user from project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User : ", username, " removed from project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -0,0 +1,22 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
func DeleteReport(c *fiber.Ctx) error {
reportID := c.Params("reportID")
reportIDInt, err := strconv.Atoi(reportID)
if err != nil {
return c.Status(400).SendString("Invalid report ID")
}
if err := db.GetDb(c).DeleteReport(reportIDInt); err != nil {
return c.Status(500).SendString((err.Error()))
}
return c.Status(200).SendString("Weekly report deleted")
}

View file

@ -0,0 +1,56 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// GetAllWeeklyReports retrieves all weekly reports for a user in a specific project
func GetAllWeeklyReports(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 and week from query parameters
projectName := c.Params("projectName")
target_user := c.Query("targetUser") // The user whose reports are being requested
// If the target user is not empty, use it as the username
if target_user == "" {
target_user = username
}
log.Info(username, " trying to get all weekly reports for: ", target_user)
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 only view their own reports
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 || target_user == username) {
log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access")
}
// Retrieve weekly reports for the user in the project from the database
reports, err := db.GetDb(c).GetAllWeeklyReports(target_user, projectName)
if err != nil {
log.Error("Error getting weekly reports for user:", target_user, "in project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning weekly report")
// Return the retrieved weekly report
return c.JSON(reports)
}

View file

@ -16,11 +16,17 @@ func GetWeeklyReport(c *fiber.Ctx) error {
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")
week := c.Query("week")
target_user := c.Query("targetUser") // The user whose report is being requested
// If the target user is not empty, use it as the username
if target_user == "" {
target_user = username
}
log.Info(username, " trying to get weekly report for: ", target_user)
if projectName == "" || week == "" {
log.Info("Missing project name or week number")
@ -34,8 +40,20 @@ func GetWeeklyReport(c *fiber.Ctx) error {
return c.Status(400).SendString("Invalid week number")
}
// If the token user is not an admin, check if the target user is the same as the token user
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 || target_user == username) {
log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access")
}
// Call the database function to get the weekly report
report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt)
report, err := db.GetDb(c).GetWeeklyReport(target_user, projectName, weekInt)
if err != nil {
log.Info("Error getting weekly report from db:", err)
return c.Status(500).SendString(err.Error())

View file

@ -1,36 +0,0 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project
func GetWeeklyReportsUserHandler(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 necessary (path) parameters from the request
projectName := c.Params("projectName")
// TODO: Here we need to check whether the user is a member of the project
// If not, we should return an error. On the other hand, if the user not a member,
// the returned list of reports will (should) allways be empty.
// Retrieve weekly reports for the user in the project from the database
reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName)
if err != nil {
log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning weekly reports for user:", username, "in project:", projectName)
// Return the list of reports as JSON
return c.JSON(reports)
}

View file

@ -0,0 +1,56 @@
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")
userNameParam := c.Query("userName")
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")
}
// Check if the user is a project manager
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())
}
// Bail if the user is not a PM or checking its own statistics
if !pm && userNameParam != "" && userNameParam != username {
log.Info("Unauthorized access for user: ", username, "trying to access project: ", projectName, "statistics for user: ", userNameParam)
return c.Status(403).SendString("Unauthorized access")
}
if pm && userNameParam != "" {
username = userNameParam
}
// Retrieve statistics for the project from the database
statistics, err := db.GetDb(c).ReportStatistics(username, projectName)
if err != nil {
log.Error("Error getting statistics for project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning statistics")
// Return the retrieved statistics
return c.JSON(statistics)
}

View file

@ -0,0 +1,41 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func UnsignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string)
// Extract report ID from the path
reportId, err := strconv.Atoi(c.Params("reportId"))
if err != nil {
log.Info("Invalid report ID")
return c.Status(400).SendString("Invalid report ID")
}
// Get the project manager's ID
projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername)
if err != nil {
log.Info("Failed to get project manager ID for user: ", projectManagerUsername)
return c.Status(500).SendString("Failed to get project manager ID")
}
// Call the database function to sign the weekly report
err = db.GetDb(c).UnsignWeeklyReport(reportId, projectManagerID)
if err != nil {
log.Info("Error Unsigning weekly report:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Project manager ID: ", projectManagerID, " unsigned report ID: ", reportId)
return c.Status(200).SendString("Weekly report unsigned successfully")
}

View file

@ -0,0 +1,42 @@
package users
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ChangeUserPassword is a handler that changes the password of a user
func ChangeUserPassword(c *fiber.Ctx) error {
//Check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
admin := claims["name"].(string)
// Extract the necessary parameters from the request
username := c.Params("username")
newPassword := c.Query("newPassword")
// Check if user is site admin
issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin)
if err != nil {
log.Warn("Error checking if siteadmin:", err)
return c.Status(500).SendString(err.Error())
} else if !issiteadmin {
log.Warn("User is not siteadmin")
return c.Status(401).SendString("User is not siteadmin")
}
// Perform the password change
err = db.GetDb(c).ChangeUserPassword(username, newPassword)
if err != nil {
log.Warn("Error changing password:", err)
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.Status(200).SendString("Password changed successfully")
}

View file

@ -0,0 +1,32 @@
package users
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
// Return the username of a user given their user id
func GetUserName(c *fiber.Ctx) error {
// Check the query params for userId
user_id_string := c.Query("userId")
if user_id_string == "" {
return c.Status(400).SendString("Missing user id")
}
// Convert to int
user_id, err := strconv.Atoi(user_id_string)
if err != nil {
return c.Status(400).SendString("Invalid user id")
}
// Get the username from the database
username, err := db.GetDb(c).GetUserName(user_id)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Send the nuclear launch codes to north korea
return c.JSON(fiber.Map{"username": username})
}

View file

@ -7,16 +7,17 @@ import (
"github.com/gofiber/fiber/v2/log"
)
// 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"
// @Produce json
// @Security JWT
// @Success 200 {array} string "Successfully returned all users"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /users/all [get]
//
// ListAllUsers returns a list of all users in the application database
func ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := db.GetDb(c).GetAllUsersApplication()

View file

@ -10,18 +10,19 @@ import (
"github.com/golang-jwt/jwt/v5"
)
// 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
// @Summary Login
// @Description Logs in a user and returns a JWT token
// @Tags Auth
// @Accept json
// @Param NewUser body types.NewUser true "login info"
// @Produce plain
// @Success 200 Token types.Token "Successfully signed token for user"
// @Produce json
// @Param body body types.NewUser true "User credentials"
// @Success 200 {object} types.Token "JWT token"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /login [post]
//
// Login logs in a user and returns a JWT token
func Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser

View file

@ -9,34 +9,40 @@ import (
"github.com/golang-jwt/jwt/v5"
)
// 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"
// @Description Renews the users token.
// @Tags Auth
// @Produce json
// @Security JWT
// @Success 200 {object} types.Token "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /loginerenew [post]
// @Router /loginrenew [post]
//
// LoginRenew renews the users token
func LoginRenew(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"])
// Renewing the token means we trust whatever is already in the token
claims := user.Claims.(jwt.MapClaims)
// 72 hour expiration time
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
// Create token with old claims, but new expiration time
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
})
// Sign it with top secret key
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
return c.SendStatus(fiber.StatusInternalServerError) // 500
}
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])

View file

@ -9,16 +9,19 @@ import (
)
// @Summary PromoteToAdmin
// @Description promote chosen user to admin
// @Description Promote chosen user to site admin
// @Tags User
// @Accept json
// @Produce plain
// @Security JWT
// @Param NewUser body types.NewUser true "user info"
// @Success 200 {json} json "Successfully promoted user"
// @Success 200 {object} types.Token "Successfully promoted user"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /promoteToAdmin [post]
//
// PromoteToAdmin promotes a user to a site admin
func PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body
var newUser types.NewUser

View file

@ -8,11 +8,9 @@ import (
"github.com/gofiber/fiber/v2/log"
)
// Register is a simple handler that registers a new user
//
// @Summary Register
// @Description Register a new user
// @Tags User
// @Tags Auth
// @Accept json
// @Produce plain
// @Param NewUser body types.NewUser true "User to register"
@ -20,6 +18,8 @@ import (
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /register [post]
//
// Register is a simple handler that registers a new user
func Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {

View file

@ -8,19 +8,19 @@ import (
"github.com/golang-jwt/jwt/v5"
)
// 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
// @Security JWT
// @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]
//
// UserDelete deletes a user from the database
func UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")

View file

@ -66,6 +66,15 @@ type WeeklyReport struct {
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 {
// The name of the project, as it appears in the database
ProjectName string `json:"projectName"`

View file

@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) {
// Should be used when registering, for example
type NewUser struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username" example:"username123"`
Password string `json:"password" example:"password123"`
}
// PublicUser represents a user that is safe to send over the API (no password)

View file

@ -25,9 +25,10 @@ import (
// @license.name AGPL
// @license.url https://www.gnu.org/licenses/agpl-3.0.html
//@securityDefinitions.apikey bererToken
//@in header
//@name Authorization
// @securityDefinitions.apikey JWT
// @in header
// @name Authorization
// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".**
// @host localhost:8080
// @BasePath /api
@ -58,13 +59,13 @@ func main() {
db := database.DbConnect(conf.DbPath)
// Migrate the database
if err = db.Migrate(); err != nil {
if err = database.Migrate(db); err != nil {
fmt.Println("Error migrating database: ", err)
os.Exit(1)
}
// 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)
os.Exit(1)
}
@ -102,33 +103,42 @@ func main() {
// userGroup := api.Group("/user") // Not currently in use
api.Get("/users/all", users.ListAllUsers)
api.Get("/project/getAllUsers", users.GetAllUsersProject)
api.Get("/username", users.GetUserName)
api.Post("/login", users.Login)
api.Post("/register", users.Register)
api.Post("/loginrenew", users.LoginRenew)
api.Post("/promoteToAdmin", users.PromoteToAdmin)
api.Put("/changeUserName", users.ChangeUserName)
api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches
api.Put("/changeUserPassword/:username", users.ChangeUserPassword)
// All project related routes
// projectGroup := api.Group("/project") // Not currently in use
api.Get("/getUserProjects", projects.GetUserProjects)
api.Get("/getProjectTimes/:projectName", projects.GetProjectTimesHandler)
api.Get("/getUserProjects/:username", projects.GetUserProjects)
api.Get("/project/:projectId", projects.GetProject)
api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler)
api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject)
api.Post("/project", projects.CreateProject)
api.Post("/ProjectRoleChange", projects.ProjectRoleChange)
api.Put("/promoteToPm/:projectName", projects.PromoteToPm)
api.Put("/addUserToProject/:projectName", projects.AddUserToProjectHandler)
api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject)
api.Delete("/removeProject/:projectName", projects.RemoveProject)
api.Delete("/project/:projectID", projects.DeleteProject)
api.Put("/changeProjectName/:projectName", projects.ChangeProjectName)
// All report related routes
// reportGroup := api.Group("/report") // Not currently in use
api.Get("/getWeeklyReport", reports.GetWeeklyReport)
api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports)
api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler)
api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports)
api.Get("/getStatistics", reports.GetStatistics)
api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport)
api.Put("/signReport/:reportId", reports.SignReport)
api.Put("/addUserToProject", projects.AddUserToProjectHandler)
api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport)
api.Put("/unsignReport/:reportId", reports.UnsignReport)
api.Delete("/deleteReport/:reportId", reports.DeleteReport)
// Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port))

2
frontend/.prettierignore Normal file
View file

@ -0,0 +1,2 @@
goTypes.ts
GenApi.ts

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,17 @@
import { AddMemberInfo } from "../Components/AddMember";
import { ProjectRoleChange } from "../Components/ChangeRole";
import { projectTimes } from "../Components/GetProjectTimes";
import { ProjectMember } from "../Components/GetUsersInProject";
import {
UpdateWeeklyReport,
NewWeeklyReport,
NewUser,
User,
Project,
NewProject,
UserProjectMember,
WeeklyReport,
StrNameChange,
NewProjMember,
Statistics,
} from "../Types/goTypes";
/**
@ -73,10 +77,7 @@ interface API {
* @param {string} token The authentication token.
* @returns {Promise<APIResponse<Project>>} A promise resolving to an API response with the created project.
*/
createProject(
project: NewProject,
token: string,
): Promise<APIResponse<Project>>;
createProject(project: NewProject, token: string): Promise<APIResponse<void>>;
/** Submits a weekly report
* @param {NewWeeklyReport} weeklyReport The weekly report object.
@ -88,16 +89,31 @@ interface API {
token: string,
): Promise<APIResponse<string>>;
/** Gets a weekly report for a specific user, project and week
/**
* Updates a weekly report.
* @param {UpdateWeeklyReport} weeklyReport The updated weekly report object.
* @param {string} token The authentication token.
* @returns {Promise<APIResponse<string>>} A promise containing the API response with the updated report.
*/
updateWeeklyReport(
weeklyReport: UpdateWeeklyReport,
token: string,
): Promise<APIResponse<string>>;
/** Gets a weekly report for a specific user, project and week.
* Keep in mind that the user within the token needs to be PM
* of the project to get the report, unless the user is the target user.
* @param {string} projectName The name of the project.
* @param {string} week The week number.
* @param {string} token The authentication token.
* @param {string} targetUser The username of the target user. Defaults to token user.
* @returns {Promise<APIResponse<WeeklyReport>>} A promise resolving to an API response with the retrieved report.
*/
getWeeklyReport(
projectName: string,
week: string,
token: string,
targetUser?: string,
): Promise<APIResponse<WeeklyReport>>;
/**
@ -107,16 +123,21 @@ interface API {
* @param {string} token The token of the user
* @returns {APIResponse<WeeklyReport[]>} A list of weekly reports
*/
getWeeklyReportsForUser(
getAllWeeklyReportsForUser(
projectName: string,
token: string,
targetUser?: string,
): Promise<APIResponse<WeeklyReport[]>>;
/** Gets all the projects of a user
* @param {string} username - The authentication token.
* @param {string} token - The authentication token.
* @returns {Promise<APIResponse<Project[]>>} A promise containing the API response with the user's projects.
*/
getUserProjects(token: string): Promise<APIResponse<Project[]>>;
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Gets a project by its id.
* @param {number} id The id of the project to retrieve.
@ -124,6 +145,16 @@ interface API {
*/
getProject(id: number): Promise<APIResponse<Project>>;
/** Gets a projects reported time
* @param {string} projectName The name of the project.
* @param {string} token The usertoken.
* @returns {Promise<APIResponse<Times>>} A promise resolving to an API response containing the project times.
*/
getProjectTimes(
projectName: string,
token: string,
): Promise<APIResponse<projectTimes>>;
/** Gets a list of all users.
* @param {string} token The authentication token of the requesting user.
* @returns {Promise<APIResponse<string[]>>} A promise resolving to an API response containing the list of users.
@ -133,7 +164,18 @@ interface API {
getAllUsersProject(
projectName: string,
token: string,
): Promise<APIResponse<UserProjectMember[]>>;
): Promise<APIResponse<ProjectMember[]>>;
/** Gets all unsigned reports in a project.
* @param {string} projectName The name of the project.
* @param {string} token The authentication token.
* @returns {Promise<APIResponse<WeeklyReport[]>>} A promise resolving to an API response containing the list of unsigned reports.
*/
getUnsignedReportsInProject(
projectName: string,
token: string,
): Promise<APIResponse<WeeklyReport[]>>;
/**
* Changes the username of a user in the database.
* @param {StrNameChange} data The object containing the previous and new username.
@ -144,10 +186,27 @@ interface API {
data: StrNameChange,
token: string,
): Promise<APIResponse<void>>;
addUserToProject(
user: NewProjMember,
/**
* Changes the role of a user in the database.
* @param {RoleChange} roleInfo The object containing the previous and new username.
* @param {string} token The authentication token.
* @returns {Promise<APIResponse<void>>} A promise resolving to an API response.
*/
changeUserRole(
roleInfo: ProjectRoleChange,
token: string,
): Promise<APIResponse<NewProjMember>>;
): Promise<APIResponse<void>>;
addUserToProject(
addMemberInfo: AddMemberInfo,
token: string,
): Promise<APIResponse<void>>;
removeUserFromProject(
user: string,
project: string,
token: string,
): Promise<APIResponse<void>>;
removeProject(
projectName: string,
@ -161,10 +220,81 @@ interface API {
* @param {number} reportId The id of the report to sign
* @param {string} token The authentication token
*/
signReport(
signReport(reportId: number, token: string): Promise<APIResponse<string>>;
/**
* Unsigns a report. Keep in mind that the user which the token belongs to must be
* the project manager of the project the report belongs to.
*
* @param {number} reportId The id of the report to sign
* @param {string} token The authentication token
*/
unsignReport(reportId: number, token: string): Promise<APIResponse<string>>;
/**
* Promotes a user to project manager within a project.
*
* @param {string} userName The username of the user to promote
* @param {string} projectName The name of the project to promote the user in
* @returns {Promise<APIResponse<string>} A promise resolving to an API response.
*/
promoteToPm(
userName: string,
projectName: string,
token: string,
): Promise<APIResponse<string>>;
/**
* Get the username from the id
* @param {number} id The id of the user
* @param {string} token Your token
*/
getUsername(id: number, token: string): Promise<APIResponse<string>>;
/**
* Deletes a WeeklyReport from the database
* @param {number} reportId The id of the report to delete
* @param {string} token The authentication token
*/
deleteWeeklyReport(
reportId: number,
token: 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,
userName?: string,
): Promise<APIResponse<Statistics>>;
/**
* Changes the name of a project
* @param {string} projectName The name of the project
* @param {string} newProjectName The new name of the project
* @param {string} token The authentication token
*/
changeProjectName(
projectName: string,
newProjectName: string,
token: string,
): Promise<APIResponse<string>>;
/**
* Changes the password of a user
* @param {string} username The username of the user
* @param {string} newPassword The new password
* @param {string} token The authentication token
*/
changeUserPassword(
username: string,
newPassword: string,
token: string,
): Promise<APIResponse<string>>;
}
/** An instance of the API */
@ -252,7 +382,7 @@ export const api: API = {
async createProject(
project: NewProject,
token: string,
): Promise<APIResponse<Project>> {
): Promise<APIResponse<void>> {
try {
const response = await fetch("/api/project", {
method: "POST",
@ -266,27 +396,28 @@ export const api: API = {
if (!response.ok) {
return { success: false, message: "Failed to create project" };
} else {
const data = (await response.json()) as Project;
return { success: true, data };
return { success: true };
}
} catch (e) {
return { success: false, message: "Failed to create project" };
return { success: false, message: "Failed to create project!" };
}
},
async addUserToProject(
user: NewProjMember,
addMemberInfo: AddMemberInfo,
token: string,
): Promise<APIResponse<NewProjMember>> {
): Promise<APIResponse<void>> {
try {
const response = await fetch("/api/addUserToProject", {
const response = await fetch(
`/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(user),
});
},
);
if (!response.ok) {
return { success: false, message: "Failed to add member" };
@ -298,6 +429,31 @@ export const api: API = {
}
},
async removeUserFromProject(
user: string,
project: string,
token: string,
): Promise<APIResponse<void>> {
try {
const response = await fetch(
`/api/removeUserFromProject/${project}?userName=${user}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to remove member" };
}
} catch (e) {
return { success: false, message: "Failed to remove member" };
}
return { success: true, message: "Removed member" };
},
async renewToken(token: string): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/loginrenew", {
@ -319,9 +475,39 @@ export const api: API = {
}
},
async getUserProjects(token: string): Promise<APIResponse<Project[]>> {
async changeUserRole(
roleInfo: ProjectRoleChange,
token: string,
): Promise<APIResponse<void>> {
try {
const response = await fetch("/api/getUserProjects", {
const response = await fetch("/api/ProjectRoleChange", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(roleInfo),
});
if (!response.ok) {
if (response.status === 403) {
return { success: false, message: "Cannot change your own role" };
}
return { success: false, message: "Could not change role" };
} else {
return { success: true };
}
} catch (e) {
return { success: false, message: "Could not change role" };
}
},
async getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>> {
try {
const response = await fetch(`/api/getUserProjects/${username}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@ -346,6 +532,37 @@ export const api: API = {
}
},
async getProjectTimes(
projectName: string,
token: string,
): Promise<APIResponse<projectTimes>> {
try {
const response = await fetch(`/api/getProjectTimes/${projectName}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return Promise.resolve({
success: false,
message:
"Fetch error: " + response.status + ", failed to get project times",
});
} else {
const data = (await response.json()) as projectTimes;
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "API error! Could not get times.",
});
}
},
async submitWeeklyReport(
weeklyReport: NewWeeklyReport,
token: string,
@ -377,14 +594,46 @@ export const api: API = {
}
},
async updateWeeklyReport(
weeklyReport: UpdateWeeklyReport,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/updateWeeklyReport", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(weeklyReport),
});
if (!response.ok) {
return {
success: false,
message: "Failed to update weekly report",
};
}
const data = await response.text();
return { success: true, message: data };
} catch (e) {
return {
success: false,
message: "Failed to update weekly report",
};
}
},
async getWeeklyReport(
projectName: string,
week: string,
token: string,
targetUser?: string,
): Promise<APIResponse<WeeklyReport>> {
try {
const response = await fetch(
`/api/getWeeklyReport?projectName=${projectName}&week=${week}`,
`/api/getWeeklyReport?projectName=${projectName}&week=${week}&targetUser=${targetUser ?? ""}`,
{
method: "GET",
headers: {
@ -405,18 +654,22 @@ export const api: API = {
}
},
async getWeeklyReportsForUser(
async getAllWeeklyReportsForUser(
projectName: string,
token: string,
targetUser?: string,
): Promise<APIResponse<WeeklyReport[]>> {
try {
const response = await fetch(`/api/getWeeklyReportsUser/${projectName}`, {
const response = await fetch(
`/api/getAllWeeklyReports/${projectName}?targetUser=${targetUser ?? ""}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
},
);
if (!response.ok) {
return {
@ -448,7 +701,11 @@ export const api: API = {
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
return {
success: false,
data: `${response.status}`,
message: "Failed to login",
};
} else {
const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token };
@ -513,7 +770,7 @@ export const api: API = {
async getAllUsersProject(
projectName: string,
token: string,
): Promise<APIResponse<UserProjectMember[]>> {
): Promise<APIResponse<ProjectMember[]>> {
try {
const response = await fetch(`/api/getUsersProject/${projectName}`, {
method: "GET",
@ -529,7 +786,7 @@ export const api: API = {
message: "Failed to get users",
});
} else {
const data = (await response.json()) as UserProjectMember[];
const data = (await response.json()) as ProjectMember[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
@ -540,6 +797,38 @@ export const api: API = {
}
},
async getUnsignedReportsInProject(
projectName: string,
token: string,
): Promise<APIResponse<WeeklyReport[]>> {
try {
const response = await fetch(`/api/getUnsignedReports/${projectName}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return {
success: false,
message:
"Failed to get unsigned reports for project: Response code " +
response.status,
};
} else {
const data = (await response.json()) as WeeklyReport[];
return { success: true, data };
}
} catch (e) {
return {
success: false,
message: "Failed to get unsigned reports for project, unknown error",
};
}
},
async changeUserName(
data: StrNameChange,
token: string,
@ -569,7 +858,7 @@ export const api: API = {
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/projectdelete/${projectName}`, {
const response = await fetch(`/api/removeProject/${projectName}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@ -615,5 +904,184 @@ export const api: API = {
} catch (e) {
return { success: false, message: "Failed to sign report" };
}
},
async unsignReport(
reportId: number,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/unsignReport/${reportId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to unsign report" };
} else {
return { success: true, message: "Report unsigned" };
}
} catch (e) {
return { success: false, message: "Failed to unsign report" };
}
},
async promoteToPm(
userName: string,
projectName: string,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(
`/api/promoteToPm/${projectName}?userName=${userName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return {
success: false,
message: "Failed to promote user to project manager",
};
}
} catch (e) {
return {
success: false,
message: "Failed to promote user to project manager",
};
}
return { success: true, message: "User promoted to project manager" };
},
async getUsername(id: number, token: string): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/username?userId=${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to get username" };
} else {
const data = (await response.json()) as string;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get username" };
}
},
async deleteWeeklyReport(
reportId: number,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/deleteReport/${reportId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to delete report" };
} else {
return { success: true, message: "Report deleted" };
}
} catch (e) {
return { success: false, message: "Failed to delete report" };
}
},
async getStatistics(
projectName: string,
token: string,
userName?: string,
): Promise<APIResponse<Statistics>> {
try {
const response = await fetch(
`/api/getStatistics/?projectName=${projectName}&userName=${userName ?? ""}`,
{
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" };
}
},
async changeProjectName(
projectName: string,
newProjectName: string,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(
`/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to change project name" };
} else {
return { success: true, message: "Project name changed" };
}
} catch (e) {
return { success: false, message: "Failed to change project name" };
}
},
async changeUserPassword(
username: string,
newPassword: string,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(
`/api/changeUserPassword/${username}?newPassword=${newPassword}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to change password" };
} else {
return { success: true, message: "Password changed" };
}
} catch (e) {
return { success: false, message: "Failed to change password" };
}
},
};

358
frontend/src/API/GenApi.ts Normal file
View file

@ -0,0 +1,358 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface TypesNewUser {
/** @example "password123" */
password?: string;
/** @example "username123" */
username?: string;
}
export interface TypesToken {
token?: string;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "//localhost:8080/api";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {},
redirect: "follow",
referrerPolicy: "no-referrer",
};
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob
? property
: typeof property === "object" && property !== null
? JSON.stringify(property)
: `${property}`,
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
};
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
};
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
}).then(async (response) => {
const r = response as HttpResponse<T, E>;
r.data = null as unknown as T;
r.error = null as unknown as E;
const data = !responseFormat
? r
: await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
if (!response.ok) throw data;
return data;
});
};
}
/**
* @title TTime API
* @version 0.0.1
* @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html)
* @baseUrl //localhost:8080/api
* @externalDocs https://swagger.io/resources/open-api/
* @contact
*
* This is the API for TTime, a time tracking application.
*/
export class GenApi<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
login = {
/**
* @description Logs in a user and returns a JWT token
*
* @tags Auth
* @name LoginCreate
* @summary Login
* @request POST:/login
*/
loginCreate: (body: TypesNewUser, params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/login`,
method: "POST",
body: body,
type: ContentType.Json,
format: "json",
...params,
}),
};
loginrenew = {
/**
* @description Renews the users token.
*
* @tags Auth
* @name LoginrenewCreate
* @summary LoginRenews
* @request POST:/loginrenew
* @secure
*/
loginrenewCreate: (params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/loginrenew`,
method: "POST",
secure: true,
format: "json",
...params,
}),
};
promoteToAdmin = {
/**
* @description Promote chosen user to site admin
*
* @tags User
* @name PromoteToAdminCreate
* @summary PromoteToAdmin
* @request POST:/promoteToAdmin
* @secure
*/
promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/promoteToAdmin`,
method: "POST",
body: NewUser,
secure: true,
type: ContentType.Json,
...params,
}),
};
register = {
/**
* @description Register a new user
*
* @tags Auth
* @name RegisterCreate
* @summary Register
* @request POST:/register
*/
registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) =>
this.request<string, string>({
path: `/register`,
method: "POST",
body: NewUser,
type: ContentType.Json,
...params,
}),
};
userdelete = {
/**
* @description UserDelete deletes a user from the database
*
* @tags User
* @name UserdeleteDelete
* @summary UserDelete
* @request DELETE:/userdelete/{username}
* @secure
*/
userdeleteDelete: (username: string, params: RequestParams = {}) =>
this.request<string, string>({
path: `/userdelete/${username}`,
method: "DELETE",
secure: true,
type: ContentType.Json,
...params,
}),
};
users = {
/**
* @description lists all users
*
* @tags User
* @name GetUsers
* @summary ListsAllUsers
* @request GET:/users/all
* @secure
*/
getUsers: (params: RequestParams = {}) =>
this.request<string[], string>({
path: `/users/all`,
method: "GET",
secure: true,
format: "json",
...params,
}),
};
}

View file

@ -1,39 +1,35 @@
import { APIResponse, api } from "../API/API";
import { NewProjMember } from "../Types/goTypes";
import { api } from "../API/API";
export interface AddMemberInfo {
userName: string;
projectName: string;
}
/**
* Tries to add a member to a project
* @param {Object} props - A NewProjMember
* @returns {boolean} True if added, false if not
* @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name
* @returns {Promise<void>}
*/
function AddMember(props: { memberToAdd: NewProjMember }): boolean {
let added = false;
if (
props.memberToAdd.username === "" ||
props.memberToAdd.role === "" ||
props.memberToAdd.projectname === ""
) {
alert("All fields must be filled before adding");
return added;
async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise<void> {
if (props.memberToAdd.userName === "") {
alert("You must choose at least one user to add");
return;
}
api
.addUserToProject(
try {
const response = await api.addUserToProject(
props.memberToAdd,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<NewProjMember>) => {
);
if (response.success) {
alert("Member added");
added = true;
alert(`[${props.memberToAdd.userName}] added`);
} else {
alert("Member not added");
alert(`[${props.memberToAdd.userName}] not added`);
console.error(response.message);
}
})
.catch((error) => {
} catch (error) {
alert(`[${props.memberToAdd.userName}] not added`);
console.error("An error occurred during member add:", error);
});
return added;
}
}
export default AddMember;

View file

@ -1,37 +1,13 @@
import { useState } from "react";
import { APIResponse, api } from "../API/API";
import { NewProject, Project } from "../Types/goTypes";
import InputField from "./InputField";
import { api } from "../API/API";
import { NewProject } from "../Types/goTypes";
import Logo from "../assets/Logo.svg";
import Button from "./Button";
/**
* Tries to add a project to the system
* @param {Object} props - Project name and description
* @returns {boolean} True if created, false if not
*/
function CreateProject(props: { name: string; description: string }): boolean {
const project: NewProject = {
name: props.name,
description: props.description,
};
let created = false;
api
.createProject(project, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<Project>) => {
if (response.success) {
created = true;
} else {
console.error(response.message);
}
})
.catch((error) => {
console.error("An error occurred during creation:", error);
});
return created;
}
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.
@ -40,42 +16,85 @@ function CreateProject(props: { name: string; description: string }): boolean {
function AddProject(): JSX.Element {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const navigate = useNavigate();
/**
* Tries to add a project to the system
*/
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 = {
name: name.replace(/ /g, ""),
description: description.trim(),
};
try {
const response = await api.createProject(
project,
localStorage.getItem("accessToken") ?? "",
);
if (response.success) {
alert(`${project.name} added!`);
setDescription("");
setName("");
navigate("/admin");
} else {
alert("Project not added, name could be taken");
console.error(response.message);
}
} catch (error) {
alert("Project not added");
console.error(error);
}
};
return (
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => {
e.preventDefault();
CreateProject({ name: name, description: description });
void handleCreateProject();
}}
>
<img
src={Logo}
className="logo w-[7vw] mb-10 mt-10"
className="logo w-[7vw] self-center mb-10 mt-10"
alt="TTIME Logo"
/>
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project
</h3>
<InputField
label="Name"
type="text"
value={name}
<ProjectNameInput
name={name}
onChange={(e) => {
e.preventDefault();
setName(e.target.value);
}}
/>
<InputField
label="Description"
type="text"
value={description}
<div className="p-2"></div>
<DescriptionInput
desc={description}
onChange={(e) => {
e.preventDefault();
setDescription(e.target.value);
}}
placeholder={"Description (Optional)"}
/>
<div className="flex items-center justify-between">
<div className="flex self-center mt-4 justify-between">
<Button
text="Create"
onClick={(): void => {

View file

@ -1,72 +1,104 @@
import { useState } from "react";
import { NewProjMember } from "../Types/goTypes";
import { useEffect, useState } from "react";
import Button from "./Button";
import AddMember, { AddMemberInfo } from "./AddMember";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import GetAllUsers from "./GetAllUsers";
import AddMember from "./AddMember";
import BackButton from "./BackButton";
import InputField from "./InputField";
/**
* Provides UI for adding a member to a project.
* @returns {JSX.Element} - Returns the component UI for adding a member
*/
function AddUserToProject(): JSX.Element {
const [name, setName] = useState("");
function AddUserToProject(props: { projectName: string }): JSX.Element {
const [names, setNames] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [role, setRole] = useState("");
GetAllUsers({ setUsersProp: setUsers });
const [usersProj, setUsersProj] = useState<ProjectMember[]>([]);
const [search, setSearch] = useState("");
const handleClick = (): boolean => {
const newMember: NewProjMember = {
username: name,
projectname: localStorage.getItem("projectName") ?? "",
role: role,
// Gets all users and project members for filtering
GetAllUsers({ setUsersProp: setUsers });
GetUsersInProject({
setUsersProp: setUsersProj,
projectName: props.projectName,
});
/*
* Filters the members from users so that users who are already
* members are not shown
*/
useEffect(() => {
setUsers((prevUsers) => {
const filteredUsers = prevUsers.filter(
(user) =>
!usersProj.some((projectUser) => projectUser.Username === user),
);
return filteredUsers;
});
}, [usersProj]);
// Attempts to add all of the selected users to the project
const handleAddClick = async (): Promise<void> => {
if (names.length === 0) {
alert("You have to choose at least one user to add");
return;
}
for (const name of names) {
const newMember: AddMemberInfo = {
userName: name,
projectName: props.projectName,
};
return AddMember({ memberToAdd: newMember });
await AddMember({ memberToAdd: newMember });
}
setNames([]);
location.reload();
};
// Updates the names that have been selected
const handleUserClick = (user: string): void => {
setNames((prevNames): string[] => {
if (!prevNames.includes(user)) {
return [...prevNames, user];
}
return prevNames.filter((name) => name !== user);
});
};
return (
<div className="border-4 border-black bg-white flex flex-col items-center justify-center rounded-3xl content-center pl-20 pr-20 h-[75vh] w-[50vh]">
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
User chosen: [{name}]
<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">
{props.projectName}
</h1>
<p className="p-1 text-center font-bold text-[26px]">
Choose users to add:
</p>
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
Role chosen: [{role}]
</p>
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
Project chosen: [{localStorage.getItem("projectName") ?? ""}]
</p>
<p className="p-1">Choose role:</p>
<div className="border-2 border-black p-2 rounded-xl text-center h-[10h] w-[16vh]">
<ul className="text-center items-center font-medium space-y-2">
<li
className="h-[10h] w-[14vh] items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
onClick={() => {
setRole("member");
<div>
<InputField
placeholder={"Search users"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
>
{"Member"}
</li>
<li
className="h-[10h] w-[14vh] items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
onClick={() => {
setRole("project_manager");
}}
>
{"Project manager"}
</li>
</ul>
</div>
<p className="p-1">Choose user:</p>
<div className="border-2 border-black p-2 rounded-xl text-center overflow-scroll h-[26vh] w-[26vh]">
<ul className="text-center font-medium space-y-2">
/>
<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>
{users.map((user) => (
{users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li
className="items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
className={
names.includes(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"
}
key={user}
value={user}
onClick={() => {
setName(user);
handleUserClick(user);
}}
>
<span>{user}</span>
@ -74,17 +106,18 @@ function AddUserToProject(): JSX.Element {
))}
</ul>
</div>
<div className="flex space-x-5 items-center justify-between">
<p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
Number of users to be added: {names.length}
</p>
<div className="space-x-10 items-center">
<Button
text="Add"
onClick={(): void => {
handleClick();
void handleAddClick();
}}
type="submit"
type="button"
/>
<BackButton />
</div>
<p className="text-center text-gray-500 text-xs"></p>
</div>
);
}

View file

@ -17,7 +17,7 @@ function AllTimeReportsInProject(): JSX.Element {
useEffect(() => {
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getWeeklyReportsForUser(
const response = await api.getAllWeeklyReportsForUser(
projectName ?? "",
token,
);
@ -37,9 +37,9 @@ function AllTimeReportsInProject(): JSX.Element {
<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}`}
to={`/editTimeReport/${projectName}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`}
key={index}
className="border-b-2 border-black w-full"
className="border-b-2 border-black w-full cursor-pointer hover:font-extrabold"
>
<div className="flex justify-between">
<h1>

View file

@ -1,8 +1,9 @@
//Info: This component is used to display all the time reports for a project. It will display the week number,
//total time spent, and if the report has been signed or not. The user can click on a report to edit it.
import { useEffect, useState } from "react";
import { NewWeeklyReport } from "../Types/goTypes";
import { WeeklyReport } from "../Types/goTypes";
import { Link, useParams } from "react-router-dom";
import { api } from "../API/API";
/**
* Renders a component that displays all the time reports for a specific project.
@ -11,15 +12,15 @@ import { Link, useParams } from "react-router-dom";
function AllTimeReportsInProject(): JSX.Element {
const { username } = useParams();
const { projectName } = useParams();
const [weeklyReports, setWeeklyReports] = useState<NewWeeklyReport[]>([]);
const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]);
/* // Call getProjects when the component mounts
useEffect(() => {
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getWeeklyReportsForUser(
const response = await api.getAllWeeklyReportsForUser(
projectName ?? "",
token,
username ?? "",
);
console.log(response);
if (response.success) {
@ -27,41 +28,10 @@ function AllTimeReportsInProject(): JSX.Element {
} else {
console.error(response.message);
}
}; */
// Mock data
const getWeeklyReports = async (): Promise<void> => {
// Simulate a delay
await Promise.resolve();
const mockWeeklyReports: NewWeeklyReport[] = [
{
projectName: "Project 1",
week: 1,
developmentTime: 10,
meetingTime: 2,
adminTime: 1,
ownWorkTime: 3,
studyTime: 4,
testingTime: 5,
},
{
projectName: "Project 1",
week: 2,
developmentTime: 8,
meetingTime: 2,
adminTime: 1,
ownWorkTime: 3,
studyTime: 4,
testingTime: 5,
},
// Add more reports as needed
];
// Use the mock data instead of the real data
setWeeklyReports(mockWeeklyReports);
};
useEffect(() => {
void getWeeklyReports();
}, []);
}, [projectName, username]);
return (
<>
@ -69,9 +39,9 @@ function AllTimeReportsInProject(): JSX.Element {
<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={`/editOthersTR/${projectName}/${username}/${newWeeklyReport.week}`}
to={`/editOthersTR/${projectName}/${username}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`}
key={index}
className="border-b-2 border-black w-full"
className="border-b-2 border-black w-full hover:font-extrabold"
>
<div className="flex justify-between">
<h1>
@ -90,7 +60,7 @@ function AllTimeReportsInProject(): JSX.Element {
</h1>
<h1>
<span className="font-bold">{"Signed: "}</span>
NO
{newWeeklyReport.signedBy ? "YES" : "NO"}
</h1>
</div>
</Link>

View file

@ -0,0 +1,36 @@
import { APIResponse, api } from "../API/API";
/**
* Changes the name of a project
* @param {string} props.projectName - Current project name
* @param {string} props.newProjectName - New project name
* @returns {void} - Nothing
*/
export default function ChangeProjectName(props: {
projectName: string;
newProjectName: string;
}): void {
if (props.projectName === "" || props.projectName === props.newProjectName) {
alert("You have to give a new name\n\nName not changed");
return;
}
api
.changeProjectName(
props.projectName,
props.newProjectName,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Name changed successfully");
location.reload();
} else {
alert("Name not changed, name could be taken");
console.error(response.message);
}
})
.catch((error) => {
alert("Name not changed");
console.error("An error occurred during change:", error);
});
}

View file

@ -0,0 +1,37 @@
import { APIResponse, api } from "../API/API";
export interface ProjectRoleChange {
username: string;
role: "project_manager" | "member" | "";
projectname: string;
}
export default function ChangeRole(roleChangeInfo: ProjectRoleChange): void {
if (
roleChangeInfo.username === "" ||
roleChangeInfo.role === "" ||
roleChangeInfo.projectname === ""
) {
// FOR DEBUG
// console.log(roleChangeInfo.role + ": Role");
// console.log(roleChangeInfo.projectname + ": P-Name");
// console.log(roleChangeInfo.username + ": U-name");
alert("You have to select a role");
return;
}
api
.changeUserRole(roleChangeInfo, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {
if (response.success) {
alert("Role changed successfully");
location.reload();
} else {
alert(response.message);
console.error(response.message);
}
})
.catch((error) => {
alert(error);
console.error("An error occurred during change:", error);
});
}

View file

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

View file

@ -0,0 +1,36 @@
import { APIResponse, api } from "../API/API";
/**
* Changes the password of a user
* @param {string} props.username - The username of the user
* @param {string} props.newPassword - The new password
* @returns {void} - Nothing
*/
export default function ChangeUserPassword(props: {
username: string;
newPassword: string;
}): void {
if (props.username === localStorage.getItem("username")) {
alert("You cannot change admin password");
return;
}
api
.changeUserPassword(
props.username,
props.newPassword,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Password changed successfully");
location.reload();
} else {
alert("Password not changed");
console.error(response.message);
}
})
.catch((error) => {
alert("Password not changed");
console.error("An error occurred during change:", error);
});
}

View file

@ -1,61 +1,33 @@
import React, { useState } from "react";
import InputField from "./InputField";
import { api } from "../API/API";
import { APIResponse, api } from "../API/API";
import { StrNameChange } from "../Types/goTypes";
function ChangeUsername(): JSX.Element {
const [newUsername, setNewUsername] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setNewUsername(e.target.value);
};
const handleSubmit = async (): Promise<void> => {
try {
// Call the API function to change the username
const token = localStorage.getItem("accessToken");
if (!token) {
throw new Error("Access token not found");
function ChangeUsername(props: { nameChange: StrNameChange }): void {
if (
props.nameChange.newName === "" ||
props.nameChange.newName === props.nameChange.prevName
) {
alert("You have to give a new name\n\nName not changed");
return;
}
const response = await api.changeUserName(
{ prevName: "currentName", newName: newUsername },
token,
);
if (props.nameChange.prevName === localStorage.getItem("username")) {
alert("You cannot change admin name");
return;
}
api
.changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {
if (response.success) {
// Optionally, add a success message or redirect the user
console.log("Username changed successfully");
alert("Name changed successfully");
location.reload();
} else {
// Handle the error message
console.error("Failed to change username:", response.message);
setErrorMessage(response.message ?? "Failed to change username");
alert("Name not changed, name could be taken");
console.error(response.message);
}
} catch (error) {
console.error("Error changing username:", error);
// Optionally, handle the error
setErrorMessage("Failed to change username");
}
};
const handleButtonClick = (): void => {
handleSubmit().catch((error) => {
console.error("Error in handleSubmit:", error);
})
.catch((error) => {
alert("Name not changed");
console.error("An error occurred during change:", error);
});
};
return (
<div>
<InputField
label="New Username"
type="text"
value={newUsername}
onChange={handleChange}
/>
{errorMessage && <div>{errorMessage}</div>}
<button onClick={handleButtonClick}>Update Username</button>
</div>
);
}
export default ChangeUsername;

View file

@ -0,0 +1,33 @@
import { api, APIResponse } from "../API/API";
/**
* Use to delete a project from the system
* @param {string} props.projectToDelete - The projectname of project to delete
* @returns {void} Nothing
* @example
* const exampleProjectName = "project";
* DeleteProject({ projectToDelete: exampleProjectName });
*/
function DeleteProject(props: { projectToDelete: string }): void {
api
.removeProject(
props.projectToDelete,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Project has been deleted!");
location.reload();
} else {
alert("Project has not been deleted");
console.error(response.message);
}
})
.catch((error) => {
alert("project has not been deleted");
console.error("An error occurred during deletion:", error);
});
}
export default DeleteProject;

View file

@ -3,7 +3,7 @@ import { api, APIResponse } from "../API/API";
/**
* Use to remove a user from the system
* @param props - The username of user to remove
* @param {string} props.usernameToDelete - The username of user to remove
* @returns {boolean} True if removed, false if not
* @example
* const exampleUsername = "user";
@ -29,7 +29,7 @@ function DeleteUser(props: { usernameToDelete: string }): boolean {
})
.catch((error) => {
alert("User has not been deleted");
console.error("An error occurred during creation:", error);
console.error("An error occurred during deletion:", error);
});
return removed;
}

View file

@ -1,93 +1,38 @@
import { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { api } from "../API/API";
import { WeeklyReport } from "../Types/goTypes";
interface UnsignedReports {
projectName: string;
username: string;
week: number;
signed: boolean;
}
/**
* Renders a component that displays the projects a user is a part of and links to the projects start-page.
* @returns The JSX element representing the component.
*/
function DisplayUserProject(): JSX.Element {
const { projectName } = useParams();
const [unsignedReports, setUnsignedReports] = useState<UnsignedReports[]>([]);
//const navigate = useNavigate();
// const getUnsignedReports = async (): Promise<void> => {
// const token = localStorage.getItem("accessToken") ?? "";
// const response = await api.getUserProjects(token);
// console.log(response);
// if (response.success) {
// setUnsignedReports(response.data ?? []);
// } else {
// console.error(response.message);
// }
// };
// const handleReportClick = async (projectName: string): Promise<void> => {
// const username = localStorage.getItem("username") ?? "";
// const token = localStorage.getItem("accessToken") ?? "";
// const response = await api.checkIfProjectManager(
// username,
// projectName,
// token,
// );
// if (response.success) {
// if (response.data) {
// navigate(`/PMProjectPage/${projectName}`);
// } else {
// navigate(`/project/${projectName}`);
// }
// } else {
// // handle error
// console.error(response.message);
// }
// };
const [unsignedReports, setUnsignedReports] = useState<WeeklyReport[]>([]);
const [usernames, setUsernames] = useState<string[]>([]);
const token = localStorage.getItem("accessToken") ?? "";
useEffect(() => {
const getUnsignedReports = async (): Promise<void> => {
// Simulate a delay
await Promise.resolve();
// Use mock data
const reports: UnsignedReports[] = [
{
projectName: "projecttest",
username: "user1",
week: 2,
signed: false,
},
{
projectName: "projecttest",
username: "user2",
week: 2,
signed: false,
},
{
projectName: "projecttest",
username: "user3",
week: 2,
signed: false,
},
{
projectName: "projecttest",
username: "user4",
week: 2,
signed: false,
},
];
// Set the state with the mock data
setUnsignedReports(reports);
const response = await api.getUnsignedReportsInProject(
projectName ?? "",
token,
);
console.log(response);
if (response.success) {
setUnsignedReports(response.data ?? []);
const usernamesPromises = (response.data ?? []).map((report) =>
api.getUsername(report.userId, token),
);
const usernamesResponses = await Promise.all(usernamesPromises);
const usernames = usernamesResponses.map(
(res) => (res.data as { username?: string }).username ?? "",
);
setUsernames(usernames);
} else {
console.error(response.message);
}
};
// Call getProjects when the component mounts
useEffect(() => {
void getUnsignedReports();
}, []);
}, [projectName, token]);
return (
<>
@ -95,23 +40,32 @@ function DisplayUserProject(): JSX.Element {
All Unsigned Reports In: {projectName}{" "}
</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[70vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[20px]">
{unsignedReports.map(
(unsignedReport: UnsignedReports, index: number) => (
{unsignedReports.map((unsignedReport: WeeklyReport, index: number) => (
<h1 key={index} className="border-b-2 border-black w-full">
<div className="flex justify-between">
<div className="flex">
<h1>{unsignedReport.username}</h1>
<span className="ml-6 mr-2 font-bold">Username:</span>
<h1>{usernames[index]}</h1>{" "}
<span className="ml-6 mr-2 font-bold">Week:</span>
<h1>{unsignedReport.week}</h1>
<span className="ml-6 mr-2 font-bold">Total Time:</span>
<h1>
{unsignedReport.developmentTime +
unsignedReport.meetingTime +
unsignedReport.adminTime +
unsignedReport.ownWorkTime +
unsignedReport.studyTime +
unsignedReport.testingTime}
</h1>
<span className="ml-6 mr-2 font-bold">Signed:</span>
<h1>NO</h1>
</div>
<div className="flex">
<div className="ml-auto flex space-x-4">
<Link
to={`/PMViewUnsignedReport/${projectName}/${unsignedReport.username}/${unsignedReport.week}`}
to={`/PMViewUnsignedReport/${projectName}/${usernames[index]}/${unsignedReport.week}`}
>
<h1 className="underline cursor-pointer font-bold">
<h1 className="cursor-pointer font-bold hover:font-extrabold hover:underline">
View Report
</h1>
</Link>
@ -119,8 +73,7 @@ function DisplayUserProject(): JSX.Element {
</div>
</div>
</h1>
),
)}
))}
</div>
</>
);

View file

@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { Project } from "../Types/goTypes";
import { useNavigate } from "react-router-dom";
import GetProjects from "./GetProjects";
import { api } from "../API/API";
/**
@ -11,22 +12,20 @@ function DisplayUserProject(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]);
const navigate = useNavigate();
const getProjects = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getUserProjects(token);
console.log(response);
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error(response.message);
}
};
GetProjects({
setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "",
});
const handleProjectClick = async (projectName: string): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.checkIfProjectManager(projectName, token);
console.log(response.data);
if (response.success) {
if (response.data) {
if (
(response.data as unknown as { isProjectManager: boolean })
.isProjectManager
) {
navigate(`/PMProjectPage/${projectName}`);
} else {
navigate(`/project/${projectName}`);
@ -37,11 +36,6 @@ function DisplayUserProject(): JSX.Element {
}
};
// Call getProjects when the component mounts
useEffect(() => {
void getProjects();
}, []);
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Your Projects</h1>
@ -51,7 +45,7 @@ function DisplayUserProject(): JSX.Element {
onClick={() => void handleProjectClick(project.name)}
key={project.id}
>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
{project.name}
</h1>
</div>

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes";
import { WeeklyReport, UpdateWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button";
@ -18,11 +18,13 @@ export default function GetWeeklyReport(): JSX.Element {
const [testingTime, setTestingTime] = useState(0);
const token = localStorage.getItem("accessToken") ?? "";
const { projectName, fetchedWeek } = useParams<{
const { projectName, fetchedWeek, signedOrUnsigned } = useParams<{
projectName: string;
fetchedWeek: string;
signedOrUnsigned: string;
}>();
console.log(projectName, fetchedWeek);
const username = localStorage.getItem("userName") ?? "";
console.log(projectName, fetchedWeek, signedOrUnsigned);
useEffect(() => {
const fetchWeeklyReport = async (): Promise<void> => {
@ -58,10 +60,11 @@ export default function GetWeeklyReport(): JSX.Element {
};
void fetchWeeklyReport();
}, [projectName, fetchedWeek, token]);
}, [projectName, fetchedWeek, signedOrUnsigned, token]);
const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
const handleUpdateWeeklyReport = async (): Promise<void> => {
const updateWeeklyReport: UpdateWeeklyReport = {
userName: username,
projectName: projectName ?? "",
week,
developmentTime,
@ -72,7 +75,7 @@ export default function GetWeeklyReport(): JSX.Element {
testingTime,
};
await api.submitWeeklyReport(newWeeklyReport, token);
await api.updateWeeklyReport(updateWeeklyReport, token);
};
const navigate = useNavigate();
@ -89,7 +92,8 @@ export default function GetWeeklyReport(): JSX.Element {
return;
}
e.preventDefault();
void handleNewWeeklyReport();
void handleUpdateWeeklyReport();
alert("Changes submitted");
navigate(-1);
}}
>
@ -128,9 +132,20 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
@ -152,9 +167,20 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
@ -176,9 +202,20 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
@ -200,9 +237,20 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
@ -224,9 +272,20 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
@ -248,14 +307,26 @@ export default function GetWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/>
</td>
</tr>
</tbody>
</table>
{signedOrUnsigned !== "signed" && (
<Button
text="Submit changes"
onClick={(): void => {
@ -263,6 +334,7 @@ export default function GetWeeklyReport(): JSX.Element {
}}
type="submit"
/>
)}
</div>
</form>
</div>

View file

@ -0,0 +1,59 @@
import { Dispatch, SetStateAction, useEffect } from "react";
import { api } from "../API/API";
/**
* Interface for reported time per category + total time reported
*/
export interface projectTimes {
admin: number;
development: number;
meeting: number;
own_work: number;
study: number;
testing: number;
totalTime?: number;
}
/**
* Gets all reported times for this project
* @param {Dispatch} props.setTimesProp - A setStateAction for the map you want to put times in
* @param {string} props.projectName - Username
* @returns {void} Nothing
* @example
* const projectName = "Example";
* const [times, setTimes] = useState<Times>();
* GetProjectTimes({ setTimesProp: setTimes, projectName: projectName });
*/
function GetProjectTimes(props: {
setTimesProp: Dispatch<SetStateAction<projectTimes | undefined>>;
projectName: string;
}): void {
const setTimes: Dispatch<SetStateAction<projectTimes | undefined>> =
props.setTimesProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getProjectTimes(props.projectName, token);
if (response.success && response.data) {
// Calculates total time reported
response.data.totalTime = response.data.admin;
response.data.totalTime += response.data.development;
response.data.totalTime += response.data.meeting;
response.data.totalTime += response.data.own_work;
response.data.totalTime += response.data.study;
response.data.totalTime += response.data.testing;
setTimes(response.data);
} else {
console.error("Failed to fetch project times:", response.message);
}
} catch (error) {
console.error("Error fetching times:", error);
}
};
void fetchUsers();
}, [props.projectName, setTimes]);
}
export default GetProjectTimes;

View file

@ -4,14 +4,17 @@ import { api } from "../API/API";
/**
* Gets all projects that user is a member of
* @param props - A setStateAction for the array you want to put projects in
* @param {Dispatch} props.setProjectsProp - A setStateAction for the array you want to put projects in
* @param {string} props.username - Username
* @returns {void} Nothing
* @example
* const username = "Example";
* const [projects, setProjects] = useState<Project[]>([]);
* GetAllUsers({ setProjectsProp: setProjects });
* GetProjects({ setProjectsProp: setProjects, username: username });
*/
function GetProjects(props: {
setProjectsProp: Dispatch<React.SetStateAction<Project[]>>;
username: string;
}): void {
const setProjects: Dispatch<React.SetStateAction<Project[]>> =
props.setProjectsProp;
@ -19,7 +22,7 @@ function GetProjects(props: {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getUserProjects(token);
const response = await api.getUserProjects(props.username, token);
if (response.success) {
setProjects(response.data ?? []);
} else {
@ -31,7 +34,7 @@ function GetProjects(props: {
};
void fetchUsers();
}, [setProjects]);
}, [props.username, setProjects]);
}
export default GetProjects;

View file

@ -1,20 +1,25 @@
import { Dispatch, useEffect } from "react";
import { UserProjectMember } from "../Types/goTypes";
import { api } from "../API/API";
export interface ProjectMember {
Username: string;
UserRole: string;
}
/**
* Gets all projects that user is a member of
* @param props - A setStateAction for the array you want to put projects in
* Gets all members of a project
* @param string - The project's name
* @param Dispatch - A setStateAction for the array you want to put members in
* @returns {void} Nothing
* @example
* const [projects, setProjects] = useState<Project[]>([]);
* GetAllUsers({ setProjectsProp: setProjects });
* const [users, setUsers] = useState<User[]>([]);
* GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
*/
function GetUsersInProject(props: {
projectName: string;
setUsersProp: Dispatch<React.SetStateAction<UserProjectMember[]>>;
setUsersProp: Dispatch<React.SetStateAction<ProjectMember[]>>;
}): void {
const setUsers: Dispatch<React.SetStateAction<UserProjectMember[]>> =
const setUsers: Dispatch<React.SetStateAction<ProjectMember[]>> =
props.setUsersProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
@ -24,10 +29,10 @@ function GetUsersInProject(props: {
if (response.success) {
setUsers(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
console.error("Failed to fetch members:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
console.error("Error fetching members:", error);
}
};
void fetchUsers();

View file

@ -1,6 +1,6 @@
//info: Header component to display the header of the page including the logo and user information where thr user can logout
import { useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import backgroundImage from "../assets/1.jpg";
/**
@ -9,23 +9,33 @@ import backgroundImage from "../assets/1.jpg";
*/
function Header(): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const username = localStorage.getItem("username");
const navigate = useNavigate();
const handleLogout = (): void => {
localStorage.clear();
};
const handleNavigation = (): void => {
if (username === "admin") {
navigate("/admin");
} else {
navigate("/yourProjects");
}
};
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(${backgroundImage})` }}
>
<Link to="/your-projects">
<div onClick={handleNavigation}>
<img
src="/src/assets/Logo.svg"
alt="TTIME Logo"
className="w-11 h-14 cursor-pointer"
/>
</Link>
</div>
<div
className="relative"

View file

@ -4,22 +4,24 @@
* @returns {JSX.Element} The input field
* @example
* <InputField
* type="text"
* label="Example"
* placeholder="New placeholder"
* type="text"
* value={example}
* onChange={(e) => {
* setExample(e.target.value);
* }}
* value={example}
* />
*/
function InputField(props: {
label: string;
type: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
label?: string;
placeholder?: string;
type?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<div className="mb-4">
<div className="">
<label
className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor={props.label}
@ -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"
id={props.label}
type={props.type}
placeholder={props.label}
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
/>

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,10 @@ function LoginCheck(props: {
password: string;
setAuthority: Dispatch<SetStateAction<number>>;
}): void {
if (props.username === "" || props.password === "") {
alert("Please enter username and password to login");
return;
}
const user: NewUser = {
username: props.username,
password: props.password,
@ -42,7 +46,15 @@ function LoginCheck(props: {
console.error("Token was undefined");
}
} 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) => {

View file

@ -25,6 +25,7 @@ function Login(props: {
}): JSX.Element {
return (
<form className="flex flex-col items-center" onSubmit={props.handleSubmit}>
<div className="space-y-3">
<InputField
type="text"
label="Username"
@ -32,6 +33,7 @@ function Login(props: {
props.setUsername(e.target.value);
}}
value={props.username}
placeholder={"Username"}
/>
<InputField
type="password"
@ -40,7 +42,9 @@ function Login(props: {
props.setPassword(e.target.value);
}}
value={props.password}
placeholder={"Password"}
/>
</div>
<Button
text="Login"
onClick={(): void => {

View file

@ -0,0 +1,78 @@
import Button from "./Button";
import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react";
import RemoveUserFromProj from "./RemoveUserFromProj";
import ChangeRoleInput from "./ChangeRoleView";
function MemberInfoModal(props: {
projectName: string;
username: string;
role: string;
onClose: () => void;
}): JSX.Element {
const [showRoles, setShowRoles] = useState(false);
const handleChangeRole = (): void => {
if (showRoles) {
setShowRoles(false);
} else {
setShowRoles(true);
}
};
return (
<div
className="fixed inset-0 bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p>
<p className="font-bold text-[20px]">{props.role}</p>
<p
className="hover:font-bold hover:cursor-pointer underline mb-2 mt-1"
onClick={handleChangeRole}
>
(Change Role)
</p>
{showRoles && (
<ChangeRoleInput
projectName={props.projectName}
username={props.username}
currentRole={props.role}
/>
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2>
<UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6">
<Button
text={"Remove"}
onClick={function (): void {
if (
window.confirm(
"Are you sure you want to remove this user from the project?",
)
) {
RemoveUserFromProj({
userToRemove: props.username,
projectName: props.projectName,
});
}
}}
type="button"
/>
<Button
text={"Close"}
onClick={function (): void {
setShowRoles(false);
props.onClose();
}}
type="button"
/>
</div>
</div>
</div>
</div>
);
}
export default MemberInfoModal;

View file

@ -0,0 +1,24 @@
import { useNavigate } from "react-router-dom";
/**
* Renders a navigation button component for navigating to
* different page
* @returns The JSX element representing the navigation button.
*/
export default function NavButton(props: {
navTo: string;
label: string;
}): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(props.navTo);
};
return (
<button
onClick={goBack}
className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;"
>
{props.label}
</button>
);
}

View file

@ -62,7 +62,7 @@ export default function NewWeeklyReport(): JSX.Element {
const success = await handleNewWeeklyReport();
if (!success) {
alert(
"A Time Report for this week already exists, please go to the edit page to edit it or change week number.",
"Error occurred! Your connection to the server might be lost or a time report for this week already exists, please check your connection or go to the edit page to edit your report or change week number.",
);
return;
}
@ -139,7 +139,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>
@ -163,7 +168,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>
@ -187,7 +197,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>
@ -211,7 +226,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>
@ -235,7 +255,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>
@ -259,7 +284,12 @@ export default function NewWeeklyReport(): JSX.Element {
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
if (
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault();
}}
/>

View file

@ -1,7 +1,8 @@
import { useState, useEffect } from "react";
import { WeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import Button from "./Button";
/**
* Renders the component for editing a weekly report.
@ -17,11 +18,14 @@ export default function OtherUsersTR(): JSX.Element {
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const [reportId, setReportId] = useState(0);
const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams();
const { username } = useParams();
const { fetchedWeek } = useParams();
const { signedOrUnsigned } = useParams();
console.log(projectName, username, fetchedWeek, signedOrUnsigned);
useEffect(() => {
const fetchUsersWeeklyReport = async (): Promise<void> => {
@ -29,6 +33,7 @@ export default function OtherUsersTR(): JSX.Element {
projectName ?? "",
fetchedWeek?.toString() ?? "0",
token,
username ?? "",
);
if (response.success) {
@ -44,6 +49,7 @@ export default function OtherUsersTR(): JSX.Element {
studyTime: 0,
testingTime: 0,
};
setReportId(report.reportId);
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
@ -59,6 +65,27 @@ export default function OtherUsersTR(): JSX.Element {
void fetchUsersWeeklyReport();
});
const handleUnsignWeeklyReport = async (): Promise<boolean> => {
const response = await api.unsignReport(reportId, token);
console.log(response);
console.log(reportId);
if (response.success) {
return true;
} else {
return false;
}
};
const handleDeleteWeeklyReport = async (): Promise<boolean> => {
const response = await api.deleteWeeklyReport(reportId, token);
console.log(response);
if (response.success) {
return true;
}
return false;
};
const navigate = useNavigate();
return (
<>
<h1 className="text-[30px] font-bold">{username}&apos;s Report</h1>
@ -86,6 +113,7 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime === 0 ? "" : developmentTime}
readOnly
/>
</td>
</tr>
@ -97,6 +125,7 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime === 0 ? "" : meetingTime}
readOnly
/>
</td>
</tr>
@ -108,6 +137,7 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime === 0 ? "" : adminTime}
readOnly
/>
</td>
</tr>
@ -119,6 +149,7 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime === 0 ? "" : ownWorkTime}
readOnly
/>
</td>
</tr>
@ -130,6 +161,7 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime === 0 ? "" : studyTime}
readOnly
/>
</td>
</tr>
@ -141,11 +173,54 @@ export default function OtherUsersTR(): JSX.Element {
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime === 0 ? "" : testingTime}
readOnly
/>
</td>
</tr>
</tbody>
</table>
<div className="flex space-x-4">
{signedOrUnsigned === "signed" && (
<Button
text="Unsign Report"
onClick={(): void => {
void (async (): Promise<void> => {
const success = await handleUnsignWeeklyReport();
if (success) {
alert("Report successfully unsigned!");
navigate(-1);
} else {
alert("Failed to unsign report");
return;
}
})();
}}
type={"button"}
/>
)}
<Button
text="Delete Time Report"
onClick={(): void => {
void (async (): Promise<void> => {
const confirmDelete = window.confirm(
"Are you sure you want to delete this report? This action cannot be undone.",
);
if (!confirmDelete) {
return;
}
const success = await handleDeleteWeeklyReport();
if (success) {
alert("Report successfully deleted!");
navigate(-1);
} else {
alert("Failed to delete report");
return;
}
})();
}}
type={"button"}
/>
</div>
</div>
</div>
</>

View file

@ -8,22 +8,22 @@ function PMProjectMenu(): JSX.Element {
<h1 className="font-bold text-[30px] 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-[5vh] p-[30px]">
<Link to={`/timeReports/${projectName}/`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Your Time Reports
</h1>
</Link>
<Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
New Time Report
</h1>
</Link>
<Link to={`/projectMembers/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Statistics
</h1>
</Link>
<Link to={`/unsignedReports/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Unsigned Time Reports
</h1>
</Link>

View file

@ -1,39 +1,149 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import Button from "./Button";
import { UserProjectMember } from "../Types/goTypes";
import GetUsersInProject from "./GetUsersInProject";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import { Link } from "react-router-dom";
import GetProjectTimes, { projectTimes } from "./GetProjectTimes";
import DeleteProject from "./DeleteProject";
import InputField from "./InputField";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
import ChangeProjectName from "./ChangeProjectName";
function ProjectInfoModal(props: {
isVisible: boolean;
projectname: string;
onClose: () => void;
onClick: (username: string) => void;
onClick: (username: string, userRole: string) => void;
}): JSX.Element {
const [users, setUsers] = useState<UserProjectMember[]>([]);
const [showInput, setShowInput] = useState(false);
const [users, setUsers] = useState<ProjectMember[]>([]);
const [times, setTimes] = useState<projectTimes>();
const [search, setSearch] = useState("");
const [newProjName, setNewProjName] = useState("");
const totalTime = useRef(0);
GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
if (!props.isVisible) return <></>;
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}?`,
)
) {
ChangeProjectName({
projectName: props.projectname,
newProjectName: newProjName,
});
} else {
alert("Name was not changed!");
}
};
useEffect(() => {
if (times?.totalTime !== undefined) {
totalTime.current = times.totalTime;
}
}, [times]);
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-2xl text-center h-[47vh] w-[40] flex flex-col">
<div className="pl-20 pr-20">
<h1 className="font-bold text-[32px] mb-[20px]">
{localStorage.getItem("projectName") ?? ""}
</h1>
<h2 className="font-bold text-[24px] mb-[20px]">Project members:</h2>
<div className="border-2 border-black p-2 rounded-lg text-center overflow-scroll h-[26vh]">
<ul className="text-left font-medium space-y-2">
<div></div>
{users.map((user) => (
<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">
<h1 className="font-bold text-[32px]">{props.projectname}</h1>
<p
className="mb-4 hover:font-bold hover:cursor-pointer hover:underline"
onClick={handleChangeNameView}
>
(Change project name)
</p>
{showInput && (
<>
<h2 className="text-[20px] font-bold pb-2">Change name:</h2>
<div className="border-2 rounded-2xl border-black px-6 pt-6 pb-1 mb-7">
<ProjectNameInput
name={newProjName}
onChange={function (e): void {
setNewProjName(e.target.value);
}}
/>
<div className="px-6 grid grid-cols-2 gap-10">
<Button
text={"Change"}
onClick={function (): void {
handleClickChangeName();
}}
type={"submit"}
/>
<Button
text={"Close"}
onClick={function (): void {
handleChangeNameView();
}}
type={"submit"}
/>
</div>
</div>
</>
)}
<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:{" "}
{Math.floor(totalTime.current / 60 / 24) + " d "}
{Math.floor((totalTime.current / 60) % 24) + " h "}
{(totalTime.current % 60) + " m "}
</p>
</div>
<h3 className="pt-7 text-[20px] font-bold">Project members:</h3>
<div className="">
<InputField
placeholder={"Search member"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="border-2 border-black mt-2 p-2 rounded-2xl text-left overflow-auto h-[24vh] font-medium space-y-2">
{users
.filter((user) => {
return search.toLowerCase() === ""
? user.Username
: user.Username.toLowerCase().includes(
search.toLowerCase(),
);
})
.map((user) => (
<li
className="items-start p-1 border-2 border-black rounded-lg bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
className="items-start px-2 py-1 border-2 border-black rounded-2xl bg-orange-200 transition-all hover:bg-orange-600 hover:text-white hover:cursor-pointer"
key={user.Username}
onClick={() => {
props.onClick(user.Username);
props.onClick(user.Username, user.UserRole);
}}
>
<span>
@ -45,16 +155,28 @@ function ProjectInfoModal(props: {
))}
</ul>
</div>
</div>
<div className="space-x-16">
<div className="space-x-5 my-2">
<Button
text={"Delete"}
onClick={function (): void {
//DELETE PROJECT
if (
window.confirm(
"Are you sure you want to delete this project?",
)
) {
DeleteProject({
projectToDelete: props.projectname,
});
}
}}
type="button"
/>
<Link to={"/adminProjectAddMember"}>
<Link
to={{
pathname: "/adminProjectAddMember",
search: props.projectname,
}}
>
<Button
text={"Add Member"}
onClick={function (): void {
@ -73,6 +195,7 @@ function ProjectInfoModal(props: {
</div>
</div>
</div>
</div>
);
}

View file

@ -1,7 +1,8 @@
import { useState } from "react";
import { NewProject } from "../Types/goTypes";
import ProjectInfoModal from "./ProjectInfoModal";
import UserInfoModal from "./UserInfoModal";
import MemberInfoModal from "./MemberInfoModal";
import InputField from "./InputField";
/**
* A list of projects for admin manage projects page, that sets an onClick
@ -18,54 +19,71 @@ export function ProjectListAdmin(props: {
projects: NewProject[];
}): JSX.Element {
const [projectModalVisible, setProjectModalVisible] = useState(false);
const [projectname, setProjectname] = useState("");
const [projectName, setProjectName] = useState("");
const [userModalVisible, setUserModalVisible] = useState(false);
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);
setUserRole(userRole);
setUserModalVisible(true);
};
const handleClickProject = (projectname: string): void => {
setProjectname(projectname);
localStorage.setItem("projectName", projectname);
setProjectName(projectname);
setProjectModalVisible(true);
};
const handleCloseProject = (): void => {
setProjectname("");
setProjectName("");
setProjectModalVisible(false);
};
const handleCloseUser = (): void => {
setProjectname("");
setUsername("");
setUserRole("");
setUserModalVisible(false);
};
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1>
{projectModalVisible && (
<ProjectInfoModal
onClose={handleCloseProject}
onClick={handleClickUser}
isVisible={projectModalVisible}
projectname={projectname}
projectname={projectName}
/>
<UserInfoModal
manageMember={true}
)}
{userModalVisible && (
<MemberInfoModal
onClose={handleCloseUser}
//TODO: CHANGE TO REMOVE USER FROM PROJECT
onDelete={() => {
return;
}}
isVisible={userModalVisible}
username={username}
projectName={projectName}
role={userRole}
/>
)}
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.projects.map((project) => (
<InputField
placeholder={"Search"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="mt-3 border-2 text-left border-black rounded-2xl px-2 divide-y divide-gray-300 font-semibold text-[30px] cursor-pointer overflow-auto h-[60vh] w-[40vw]">
{props.projects
.filter((project) => {
return search.toLowerCase() === ""
? project.name
: project.name.toLowerCase().includes(search.toLowerCase());
})
.map((project) => (
<li
className="pt-5"
className="hover:font-extrabold hover:underline p-1"
key={project.name}
onClick={() => {
handleClickProject(project.name);

View file

@ -1,39 +1,43 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import { api } from "../API/API";
import { UserProjectMember } from "../Types/goTypes";
function ProjectMembers(): JSX.Element {
const { projectName } = useParams();
const [projectMembers, setProjectMembers] = useState<UserProjectMember[]>([]);
const [projectMembers, setProjectMembers] = useState<ProjectMember[]>([]);
useEffect(() => {
const getProjectMembers = async (): Promise<void> => {
GetUsersInProject({
projectName: projectName ?? "",
setUsersProp: setProjectMembers,
});
const handleUserDeleteClick = async (username: string): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllUsersProject(projectName ?? "", token);
console.log(response);
if (response.success) {
setProjectMembers(response.data ?? []);
} else {
console.error(response.message);
}
const response = await api.removeUserFromProject(
username,
projectName ?? "",
token,
);
console.log(response.data);
// Remove the deleted user from the state
setProjectMembers((prevMembers) =>
prevMembers.filter((member) => member.Username !== username),
);
};
void getProjectMembers();
}, [projectName]);
interface ProjectMember {
Username: string;
UserRole: string;
}
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">
All Members In: {projectName}{" "}
</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[70vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[20px]">
{projectMembers.map((projectMember: ProjectMember, index: number) => (
{projectMembers.map((projectMember: ProjectMember, index: number) => {
if (projectMember.Username === "admin") {
return null; // Skip rendering for admin user
}
return (
<h1 key={index} className="border-b-2 border-black w-full">
<div className="flex justify-between">
<div className="flex">
@ -43,10 +47,24 @@ function ProjectMembers(): JSX.Element {
</div>
<div className="flex">
<div className="ml-auto flex space-x-4">
{projectMember.Username !==
localStorage.getItem("username") && (
<h1
className="cursor-pointer font-bold hover:font-extrabold hover:underline"
onClick={() => {
confirm(
"Are you sure you want to delete this user? This action cannot be undone.",
) &&
void handleUserDeleteClick(projectMember.Username);
}}
>
Delete User
</h1>
)}
<Link
to={`/otherUsersTimeReports/${projectName}/${projectMember.Username}`}
>
<h1 className="underline cursor-pointer font-bold">
<h1 className="cursor-pointer font-bold hover:font-extrabold hover:underline">
View Reports
</h1>
</Link>
@ -54,7 +72,8 @@ function ProjectMembers(): JSX.Element {
</div>
</div>
</h1>
))}
);
})}
</div>
</>
);

View file

@ -3,27 +3,52 @@ import { NewUser } from "../Types/goTypes";
import { api } from "../API/API";
import Logo from "../assets/Logo.svg";
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.
* @returns The JSX element representing the registration form.
*/
export default function Register(): JSX.Element {
const [username, setUsername] = useState<string>();
const [password, setPassword] = useState<string>();
const [errMessage, setErrMessage] = useState<string>();
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [errMessage, setErrMessage] = useState<string>("");
const handleRegister = async (): Promise<void> => {
if (
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;
}
const newUser: NewUser = {
username: username ?? "",
password: password ?? "",
username: username,
password: password,
};
const response = await api.registerUser(newUser);
if (response.success) {
alert("User added!");
alert(`${newUser.username} added!`);
setPassword("");
setUsername("");
} else {
alert("User not added");
alert("User not added, name could be taken");
setErrMessage(response.message ?? "Unknown error");
console.error(errMessage);
}
@ -33,7 +58,7 @@ export default function Register(): JSX.Element {
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => {
e.preventDefault();
void handleRegister();
@ -41,29 +66,28 @@ export default function Register(): JSX.Element {
>
<img
src={Logo}
className="logo w-[7vw] mb-10 mt-10"
className="logo self-center w-[7vw] mb-10 mt-10"
alt="TTIME Logo"
/>
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Register New User
</h3>
<InputField
label="Username"
type="text"
value={username ?? ""}
<UsernameInput
username={username}
onChange={(e) => {
setUsername(e.target.value);
}}
/>
<InputField
label="Password"
type="password"
value={password ?? ""}
<div className="py-2" />
<PasswordInput
password={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<div className="flex items-center justify-between">
<div className="flex self-center justify-between">
<Button
text="Register"
onClick={(): void => {

View file

@ -0,0 +1,41 @@
import { api, APIResponse } from "../API/API";
/**
* Removes a user from a project
* @param {string} props.usernameToDelete - The username of user to remove
* @param {string} props.projectName - Project to remove user from
* @returns {void}
* @example
* const exampleUsername = "user";
* const exampleProjectName "project";
* RemoveUserFromProj({ userToRemove: exampleUsername, projectName: exampleProjectName });
*/
export default function RemoveUserFromProj(props: {
userToRemove: string;
projectName: string;
}): void {
if (props.userToRemove === localStorage.getItem("username")) {
alert("Cannot remove yourself");
return;
}
api
.removeUserFromProject(
props.userToRemove,
props.projectName,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<void>) => {
if (response.success) {
alert(`${props.userToRemove} has been removed!`);
location.reload();
} else {
alert(`${props.userToRemove} has not been removed due to an error`);
console.error(response.message);
}
})
.catch((error) => {
alert(`${props.userToRemove} has not been removed due to an error`);
console.error("An error occurred during deletion:", error);
});
}

View file

@ -1,70 +1,46 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { api } from "../API/API";
import { projectTimes } from "./GetProjectTimes";
/**
* Renders the component for showing total time per role in a project.
* @returns JSX.Element
*/
export default function TimePerRole(): JSX.Element {
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 [development, setDevelopment] = useState<number>(0);
const [meeting, setMeeting] = useState<number>(0);
const [admin, setAdmin] = useState<number>(0);
const [own_work, setOwnWork] = useState<number>(0);
const [study, setStudy] = useState<number>(0);
const [testing, setTesting] = useState<number>(0);
const total = development + meeting + admin + own_work + study + testing;
// const token = localStorage.getItem("accessToken") ?? "";
// const username = localStorage.getItem("username") ?? "";
const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams();
// const fetchTimePerRole = async (): Promise<void> => {
// const response = await api.getTimePerRole(
// username,
// projectName ?? "",
// token,
// );
// {
// if (response.success) {
// const report: TimePerRole = response.data ?? {
// PManagerTime: 0,
// SManagerTime: 0,
// DeveloperTime: 0,
// TesterTime: 0,
// };
// } else {
// console.error("Failed to fetch weekly report:", response.message);
// }
// }
interface TimePerActivity {
developmentTime: number;
meetingTime: number;
adminTime: number;
ownWorkTime: number;
studyTime: number;
testingTime: number;
}
const fetchTimePerActivity = async (): Promise<void> => {
// Use mock data
const report: TimePerActivity = {
developmentTime: 100,
meetingTime: 200,
adminTime: 300,
ownWorkTime: 50,
studyTime: 75,
testingTime: 110,
const response = await api.getProjectTimes(projectName ?? "", token);
{
if (response.success) {
const report: projectTimes = response.data ?? {
development: 0,
meeting: 0,
admin: 0,
own_work: 0,
study: 0,
testing: 0,
};
// Set the state with the mock data
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
setAdminTime(report.adminTime);
setOwnWorkTime(report.ownWorkTime);
setStudyTime(report.studyTime);
setTestingTime(report.testingTime);
await Promise.resolve();
setDevelopment(report.development);
setMeeting(report.meeting);
setAdmin(report.admin);
setOwnWork(report.own_work);
setStudy(report.study);
setTesting(report.testing);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
}
};
useEffect(() => {
@ -74,7 +50,7 @@ export default function TimePerRole(): JSX.Element {
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">
Total Time Per Activity In: {projectName}{" "}
Total Time Per Activity For All Members In: {projectName}{" "}
</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<div className="flex flex-col items-center">
@ -94,10 +70,8 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={development}
readOnly
/>
</td>
</tr>
@ -107,10 +81,8 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={meeting}
readOnly
/>
</td>
</tr>
@ -120,10 +92,8 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={admin}
readOnly
/>
</td>
</tr>
@ -133,10 +103,8 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={own_work}
readOnly
/>
</td>
</tr>
@ -146,10 +114,8 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={study}
readOnly
/>
</td>
</tr>
@ -159,13 +125,17 @@ export default function TimePerRole(): JSX.Element {
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime}
onKeyDown={(event) => {
event.preventDefault();
}}
value={testing}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh] font-bold font-">
<td>In Total:</td>
<td>
<h1>{total}</h1>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -1,51 +1,177 @@
import { Link } from "react-router-dom";
import Button from "./Button";
import DeleteUser from "./DeleteUser";
import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react";
import ChangeUsername from "./ChangeUsername";
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";
import ChangeUserPassword from "./ChangeUserPassword";
function UserInfoModal(props: {
isVisible: boolean;
manageMember: boolean;
username: string;
onClose: () => void;
onDelete: (username: string) => void;
}): JSX.Element {
if (!props.isVisible) return <></>;
const ManageUserOrMember = (check: boolean): JSX.Element => {
if (check) {
return (
<Link to="/AdminChangeRole">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Role)
</p>
</Link>
);
const [showNameInput, setShowNameInput] = useState(false);
const [showPwordInput, setShowPwordInput] = useState(false);
const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
if (!props.isVisible) {
return <></>;
}
/*
* Switches name input between visible/invisible
* and makes password input invisible
*/
const handleShowNameInput = (): void => {
if (showPwordInput) setShowPwordInput(false);
if (showNameInput) {
setShowNameInput(false);
setNewUsername("");
} else {
setShowNameInput(true);
setNewPassword("");
}
return (
<Link to="/AdminChangeUserName">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Username)
</p>
</Link>
);
};
/*
* 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 => {
if (
!alphanumeric.test(newUsername) ||
newUsername.length > usernameUpLimit ||
newUsername.length < usernameLowLimit
) {
alert(
"Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(
`Do you really want to change username of ${props.username} to ${newUsername}?`,
)
) {
const nameChange: StrNameChange = {
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}?`)
) {
ChangeUserPassword({
username: props.username,
newPassword: newPassword,
});
} else {
alert("Password was not changed!");
}
};
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 flex flex-col">
<div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p>
{ManageUserOrMember(props.manageMember)}
<div>
<h2 className="font-bold text-[22px] mb-[20px]">
Member of these projects:
</h2>
<div className="pr-6 pl-6">
<UserProjectListAdmin />
<p className="mt-2 font-bold text-[20px]">Change:</p>
<p className="mt-2 space-x-3 mb-[10px]">
<span
className={
showNameInput
? "items-start font-semibold py-1 px-2 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start font-medium py-1 px-2 border-2 border-gray-500 text-white rounded-full bg-orange-300 hover:bg-orange-400 transition-all hover:text-gray-100 hover:border-gray-600 hover:cursor-pointer"
}
onClick={handleShowNameInput}
>
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>
{showNameInput && (
<div className="mt-7">
<UsernameInput
username={newUsername}
onChange={(e) => {
setNewUsername(e.target.value);
}}
/>
<Button
text={"Change"}
onClick={function (): void {
handleClickChangeName();
}}
type={"submit"}
/>
</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>
<div className="items-center space-x-6 pr-6 pl-6">
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2>
<UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6">
<Button
text={"Delete"}
onClick={function (): void {
@ -62,6 +188,10 @@ function UserInfoModal(props: {
<Button
text={"Close"}
onClick={function (): void {
setNewUsername("");
setNewPassword("");
setShowNameInput(false);
setShowPwordInput(false);
props.onClose();
}}
type="button"
@ -69,6 +199,7 @@ function UserInfoModal(props: {
</div>
</div>
</div>
</div>
);
}

View file

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

View file

@ -1,35 +1,17 @@
import { useEffect, useState } from "react";
import { api } from "../API/API";
import { useState } from "react";
import { Project } from "../Types/goTypes";
import GetProjects from "./GetProjects";
function UserProjectListAdmin(): JSX.Element {
function UserProjectListAdmin(props: { username: string }): 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();
}, []);
GetProjects({ setProjectsProp: setProjects, username: props.username });
return (
<div className="border-2 border-black bg-white p-2 rounded-lg text-center">
<ul>
<div className="border-2 border-black bg-white rounded-2xl text-left overflow-auto h-[15vh] font-medium">
<ul className="divide-y-2">
{projects.map((project) => (
<li key={project.id}>
<li className="mx-2 my-1" key={project.id}>
<span>{project.name}</span>
</li>
))}

View file

@ -16,12 +16,12 @@ function UserProjectMenu(): JSX.Element {
<h1 className="font-bold text-[30px] 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={`/timeReports/${projectName}/`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Your Time Reports
</h1>
</Link>
<Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
New Time Report
</h1>
</Link>

View file

@ -0,0 +1,150 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { api } from "../API/API";
import { Statistics } from "../Types/goTypes";
/**
* Renders the component for showing total time per role in a project.
* @returns JSX.Element
*/
export default function UserStatistics(): JSX.Element {
const [development, setDevelopment] = useState<number>(0);
const [meeting, setMeeting] = useState<number>(0);
const [admin, setAdmin] = useState<number>(0);
const [own_work, setOwnWork] = useState<number>(0);
const [study, setStudy] = useState<number>(0);
const [testing, setTesting] = useState<number>(0);
const total = development + meeting + admin + own_work + study + testing;
const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams();
const { username } = useParams();
const fetchTimePerActivity = async (): Promise<void> => {
const response = await api.getStatistics(
projectName ?? "",
token,
username ?? "",
);
{
if (response.success) {
const statistics: Statistics = response.data ?? {
totalDevelopmentTime: 0,
totalMeetingTime: 0,
totalAdminTime: 0,
totalOwnWorkTime: 0,
totalStudyTime: 0,
totalTestingTime: 0,
};
setDevelopment(statistics.totalDevelopmentTime);
setMeeting(statistics.totalMeetingTime);
setAdmin(statistics.totalAdminTime);
setOwnWork(statistics.totalOwnWorkTime);
setStudy(statistics.totalStudyTime);
setTesting(statistics.totalTestingTime);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
}
};
useEffect(() => {
void fetchTimePerActivity();
});
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">
Total Time In: {projectName}{" "}
</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<div className="flex flex-col items-center">
<table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead>
<tr>
<th className="w-1/2 py-2 border-b-2 border-black">Activity</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
<tr className="h-[10vh]">
<td>Development</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={development}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={meeting}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={admin}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={own_work}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={study}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="string"
className="border-2 border-black rounded-md text-center w-1/2"
value={testing}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh] font-bold font-">
<td>In Total:</td>
<td>
<h1>{total}</h1>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
);
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes";
import { WeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button";
@ -18,6 +18,7 @@ export default function GetOtherUsersReport(): JSX.Element {
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const [reportId, setReportId] = useState(0);
const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams();
@ -30,8 +31,9 @@ export default function GetOtherUsersReport(): JSX.Element {
projectName ?? "",
fetchedWeek?.toString() ?? "0",
token,
username ?? "",
);
console.log(response);
if (response.success) {
const report: WeeklyReport = response.data ?? {
reportId: 0,
@ -45,6 +47,7 @@ export default function GetOtherUsersReport(): JSX.Element {
studyTime: 0,
testingTime: 0,
};
setReportId(report.reportId);
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
@ -60,32 +63,33 @@ export default function GetOtherUsersReport(): JSX.Element {
void fetchUsersWeeklyReport();
});
const handleSignWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = {
projectName: projectName ?? "",
week,
developmentTime,
meetingTime,
adminTime,
ownWorkTime,
studyTime,
testingTime,
};
await api.submitWeeklyReport(newWeeklyReport, token);
const handleSignWeeklyReport = async (): Promise<boolean> => {
const response = await api.signReport(reportId, token);
if (response.success) {
return true;
} else {
return false;
}
};
const navigate = useNavigate();
return (
<>
<h1 className="text-[30px] font-bold">{username}&apos;s Report</h1>
<h1 className="text-[30px] font-bold"> {username}&apos;s Report</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form
onSubmit={(e) => {
e.preventDefault();
void handleSignWeeklyReport();
void (async (): Promise<void> => {
const success = await handleSignWeeklyReport();
if (!success) {
alert("Failed to sign report!");
return;
}
alert("Report successfully signed!");
navigate(-1);
})();
}}
>
<div className="flex flex-col items-center">
@ -112,7 +116,10 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime === 0 ? "" : developmentTime}
defaultValue={
developmentTime === 0 ? "" : developmentTime
}
readOnly
/>
</td>
</tr>
@ -123,7 +130,8 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime === 0 ? "" : meetingTime}
defaultValue={meetingTime === 0 ? "" : meetingTime}
readOnly
/>
</td>
</tr>
@ -134,7 +142,8 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime === 0 ? "" : adminTime}
defaultValue={adminTime === 0 ? "" : adminTime}
readOnly
/>
</td>
</tr>
@ -145,7 +154,8 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime === 0 ? "" : ownWorkTime}
defaultValue={ownWorkTime === 0 ? "" : ownWorkTime}
readOnly
/>
</td>
</tr>
@ -156,7 +166,8 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime === 0 ? "" : studyTime}
defaultValue={studyTime === 0 ? "" : studyTime}
readOnly
/>
</td>
</tr>
@ -167,7 +178,8 @@ export default function GetOtherUsersReport(): JSX.Element {
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime === 0 ? "" : testingTime}
defaultValue={testingTime === 0 ? "" : testingTime}
readOnly
/>
</td>
</tr>

View file

@ -0,0 +1,23 @@
import { useEffect } from "react";
import { GenApi } from "../API/GenApi";
// Instantiation of the API
const api = new GenApi();
export function GenApiDemo(): JSX.Element {
// Sync wrapper around the loginCreate method
const register = async (): Promise<void> => {
const response = await api.login.loginCreate({
username: "admin",
password: "admin",
});
console.log(response.data); // This should be the inner type of the response
};
// Call the wrapper
useEffect(() => {
void register();
});
return <></>;
}

View file

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

View file

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

View file

@ -1,28 +0,0 @@
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 = (
<>
<ChangeUsername />
</>
);
const buttons = (
<>
<Button
text="Finish"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminChangeUsername;

View file

@ -1,23 +1,19 @@
import { Link } from "react-router-dom";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import { ProjectListAdmin } from "../../Components/ProjectListAdmin";
import { Project } from "../../Types/goTypes";
import GetProjects from "../../Components/GetProjects";
import { useState } from "react";
import NavButton from "../../Components/NavButton";
function AdminManageProjects(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]);
GetProjects({ setProjectsProp: setProjects });
const content = (
<>
<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>
</>
);
GetProjects({
setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "",
});
const content = <ProjectListAdmin projects={projects} />;
const buttons = (
<>
@ -30,7 +26,7 @@ function AdminManageProjects(): JSX.Element {
type="button"
/>
</Link>
<BackButton />
<NavButton navTo="/admin" label={"Back"} />
</>
);

View file

@ -12,14 +12,7 @@ function AdminManageUsers(): JSX.Element {
const navigate = useNavigate();
const content = (
<>
<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 content = <UserListAdmin users={users} />;
const buttons = (
<>

View file

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

View file

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

View file

@ -1,23 +0,0 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
function AdminProjectChangeUserRole(): JSX.Element {
const content = <></>;
const buttons = (
<>
<Button
text="Change"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectChangeUserRole;

View file

@ -1,23 +0,0 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
function AdminProjectManageMembers(): JSX.Element {
const content = <></>;
const buttons = (
<>
<Button
text="Add Member"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectManageMembers;

View file

@ -1,33 +0,0 @@
import { useParams } from "react-router-dom";
import { api } from "../../API/API";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
async function handleDeleteProject(
projectName: string,
token: string,
): Promise<void> {
await api.removeProject(projectName, token);
}
function AdminProjectPage(): JSX.Element {
const content = <></>;
const { projectName } = useParams();
const token = localStorage.getItem("accessToken");
const buttons = (
<>
<Button
text="Delete"
onClick={() => handleDeleteProject(projectName, token)}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectPage;

View file

@ -1,23 +0,0 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
function AdminProjectViewMemberInfo(): JSX.Element {
const content = <></>;
const buttons = (
<>
<Button
text="Remove"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectViewMemberInfo;

View file

@ -1,28 +0,0 @@
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 = (
<>
<UserProjectListAdmin />
</>
);
const buttons = (
<>
<Button
text="Delete"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminViewUserInfo;

View file

@ -1,8 +1,12 @@
import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
import AllTimeReportsInProjectOtherUser from "../../Components/AllTimeReportsInProjectOtherUser";
import Button from "../../Components/Button";
import { useParams, Link } from "react-router-dom";
function PMOtherUsersTR(): JSX.Element {
const { projectName } = useParams();
const { username } = useParams();
const content = (
<>
<AllTimeReportsInProjectOtherUser />
@ -11,6 +15,15 @@ function PMOtherUsersTR(): JSX.Element {
const buttons = (
<>
<Link to={`/viewStatistics/${projectName}/${username}`}>
<Button
text={`Statistics: ${username}`}
onClick={(): void => {
return;
}}
type={"button"}
/>
</Link>
<BackButton />
</>
);

View file

@ -16,7 +16,7 @@ function PMProjectMembers(): JSX.Element {
<>
<Link to={`/PMtimeactivity/${projectName}`}>
<Button
text="Time / Activity"
text="Statistics"
onClick={(): void => {
return;
}}

View file

@ -1,8 +1,13 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import UserStatistics from "../../Components/UserStatistics";
function AdminProjectStatistics(): JSX.Element {
const content = <></>;
function UserNewTimeReportPage(): JSX.Element {
const content = (
<>
<UserStatistics />
</>
);
const buttons = (
<>
@ -12,4 +17,4 @@ function AdminProjectStatistics(): JSX.Element {
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectStatistics;
export default UserNewTimeReportPage;

View file

@ -1,10 +1,12 @@
import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
import { useParams } from "react-router-dom";
import { useParams, Link } from "react-router-dom";
import AllTimeReportsInProject from "../../Components/AllTimeReportsInProject";
import Button from "../../Components/Button";
function UserViewTimeReportsPage(): JSX.Element {
const { projectName } = useParams();
const username = localStorage.getItem("username");
const content = (
<>
@ -17,6 +19,15 @@ function UserViewTimeReportsPage(): JSX.Element {
const buttons = (
<>
<Link to={`/viewStatistics/${projectName}/${username}`}>
<Button
text="Statistics"
onClick={(): void => {
return;
}}
type={"button"}
/>
</Link>
<BackButton />
</>
);

View file

@ -124,6 +124,52 @@ export interface WeeklyReport {
*/
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 {
/**
* The name of the project, as it appears in the database
*/
projectName: string;
/**
* The name of the user
*/
userName: string;
/**
* The week number
*/
week: number /* int */;
/**
* Total time spent on development
*/
developmentTime: number /* int */;
/**
* Total time spent in meetings
*/
meetingTime: number /* int */;
/**
* Total time spent on administrative tasks
*/
adminTime: number /* int */;
/**
* Total time spent on personal projects
*/
ownWorkTime: number /* int */;
/**
* Total time spent on studying
*/
studyTime: number /* int */;
/**
* Total time spent on testing
*/
testingTime: number /* int */;
}
//////////
// source: project.go
@ -151,16 +197,9 @@ export interface NewProject {
*/
export interface RoleChange {
username: string;
role: "project_manager" | "user";
role: 'project_manager' | 'user';
projectname: string;
}
export interface NewProjMember {
username: string;
projectname: string;
role: string;
}
export interface NameChange {
id: number /* int */;
name: string;
@ -191,11 +230,6 @@ export interface PublicUser {
userId: string;
username: string;
}
export interface UserProjectMember {
Username: string;
UserRole: string;
}
/**
* wrapper type for token
*/

View file

@ -9,6 +9,7 @@ import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx";
import UserEditTimeReportPage from "./Pages/UserPages/UserEditTimeReportPage.tsx";
import UserNewTimeReportPage from "./Pages/UserPages/UserNewTimeReportPage.tsx";
import UserViewTimeReportsPage from "./Pages/UserPages/UserViewTimeReportsPage.tsx";
import UserViewStatistics from "./Pages/UserPages/UserViewStatistics.tsx";
import PMChangeRole from "./Pages/ProjectManagerPages/PMChangeRole.tsx";
import PMOtherUsersTR from "./Pages/ProjectManagerPages/PMOtherUsersTR.tsx";
import PMProjectMembers from "./Pages/ProjectManagerPages/PMProjectMembers.tsx";
@ -18,17 +19,10 @@ import PMTotalTimeRole from "./Pages/ProjectManagerPages/PMTotalTimeRole.tsx";
import PMUnsignedReports from "./Pages/ProjectManagerPages/PMUnsignedReports.tsx";
import PMViewUnsignedReport from "./Pages/ProjectManagerPages/PMViewUnsignedReport.tsx";
import AdminManageUsers from "./Pages/AdminPages/AdminManageUsers.tsx";
import AdminViewUserInfo from "./Pages/AdminPages/AdminViewUserInfo.tsx";
import AdminManageProjects from "./Pages/AdminPages/AdminManageProjects.tsx";
import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx";
import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx";
import AdminChangeUsername from "./Pages/AdminPages/AdminChangeUsername.tsx";
import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx";
import AdminProjectChangeUserRole from "./Pages/AdminPages/AdminProjectChangeUserRole.tsx";
import AdminProjectManageMembers from "./Pages/AdminPages/AdminProjectManageMembers.tsx";
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";
import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx";
import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx";
@ -61,9 +55,13 @@ const router = createBrowserRouter([
element: <UserViewTimeReportsPage />,
},
{
path: "/editTimeReport/:projectName/:fetchedWeek",
path: "/editTimeReport/:projectName/:fetchedWeek/:signedOrUnsigned",
element: <UserEditTimeReportPage />,
},
{
path: "/viewStatistics/:projectName/:username",
element: <UserViewStatistics />,
},
{
path: "/changeRole/:projectName/:username",
element: <PMChangeRole />,
@ -73,7 +71,7 @@ const router = createBrowserRouter([
element: <PMOtherUsersTR />,
},
{
path: "/editOthersTR/:projectName/:username/:fetchedWeek",
path: "/editOthersTR/:projectName/:username/:fetchedWeek/:signedOrUnsigned",
element: <PMViewOtherUsersTR />,
},
{
@ -100,34 +98,10 @@ const router = createBrowserRouter([
path: "/PMViewUnsignedReport/:projectName/:username/:fetchedWeek",
element: <PMViewUnsignedReport />,
},
{
path: "/adminChangeUsername",
element: <AdminChangeUsername />,
},
{
path: "/adminProjectAddMember",
element: <AdminProjectAddMember />,
},
{
path: "/adminProjectChangeUserRole",
element: <AdminProjectChangeUserRole />,
},
{
path: "/adminProjectManageMembers",
element: <AdminProjectManageMembers />,
},
{
path: "/adminProjectPage",
element: <AdminProjectPage />,
},
{
path: "/adminProjectStatistics",
element: <AdminProjectStatistics />,
},
{
path: "/adminProjectViewMembers",
element: <AdminProjectViewMemberInfo />,
},
{
path: "/addProject",
element: <AdminAddProject />,
@ -136,10 +110,6 @@ const router = createBrowserRouter([
path: "/adminAddUser",
element: <AdminAddUser />,
},
{
path: "/adminUserInfo",
element: <AdminViewUserInfo />,
},
{
path: "/adminManageProject",
element: <AdminManageProjects />,

191
testing/helpers.py Normal file
View file

@ -0,0 +1,191 @@
import requests
import string
import random
import json
# Helper function for the TTime API testing suite
# For style guide, see:
# https://peps.python.org/pep-0008/#function-and-variable-names
# https://google.github.io/styleguide/pyguide.html#316-naming
##################
## Static Paths ##
##################
base_url = "http://localhost:8080"
registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project"
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"
getAllWeeklyReportsPath = base_url + "/api/getAllWeeklyReports"
checkIfProjectManagerPath = base_url + "/api/checkIfProjectManager"
ProjectRoleChangePath = base_url + "/api/ProjectRoleChange"
getUsersProjectPath = base_url + "/api/getUsersProject"
getUnsignedReportsPath = base_url + "/api/getUnsignedReports"
getChangeUserNamePath = base_url + "/api/changeUserName"
getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport"
removeProjectPath = base_url + "/api/removeProject"
promoteToPmPath = base_url + "/api/promoteToPm"
unsignReportPath = base_url + "/api/unsignReport"
deleteReportPath = base_url + "/api/deleteReport"
getStatisticsPath = base_url + "/api/getStatistics"
changeProjectNamePath = base_url + "/api/changeProjectName"
changeUserPasswordPath = base_url + "/api/changeUserPassword"
debug_output = False
def gprint(*args, **kwargs):
print("\033[92m", *args, "\033[00m", **kwargs)
def dprint(*args, **kwargs):
if debug_output:
print(*args, **kwargs)
def randomString(len=10):
"""Generate a random string of fixed length"""
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(len))
############ ############ ############ ############ ############
# Posts the username and password to the register endpoint
def register(username: string, password: string):
dprint("Registering with username: ", username, " and password: ", password)
response = requests.post(
registerPath, json={"username": username, "password": password}
)
dprint(response.text)
return response
# Posts the username and password to the login endpoint
def login(username: string, password: string):
dprint("Logging in with username: ", username, " and password: ", password)
response = requests.post(
loginPath, json={"username": username, "password": password}
)
dprint(response.text)
return response
# Register a user and return the token
def register_and_login(username: string, password: string) -> string:
register(username, password)
response = login(username, password)
return response.json()["token"]
def create_project(
token: string, project_name: string, description: string = "Test description"
):
dprint("Creating project with name: ", project_name)
response = requests.post(
addProjectPath,
headers={"Authorization": "Bearer " + token},
json={"name": project_name, "description": description},
)
dprint(response.text)
return response
# Add a user to a project, requires the user withing the token to be a project manager of said project
def addToProject(token: string, username: string, project_name: string):
dprint("Adding user with username: ", username, " to project: ", project_name)
response = requests.put(
addUserToProjectPath + "/" + project_name,
headers={"Authorization": "Bearer " + token},
params={"userName": username},
)
dprint(response.text)
return response
def promoteToManager(token: string, username: string, project_name: string):
dprint(
"Promoting user with username: ",
username,
" to project manager of project: ",
project_name,
)
response = requests.put(
promoteToPmPath + "/" + project_name,
headers={"Authorization": "Bearer " + token},
params={"userName": username},
)
dprint(response.text)
return response
def submitReport(token: string, report):
dprint("Submitting report: ", report)
response = requests.post(
submitReportPath,
json=report,
headers={"Authorization": "Bearer " + token},
)
return response
def getReport(token: string, username: string, projectName: string):
# Retrieve the report ID
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName, "week": 1},
)
return response.json()
def signReport(project_manager_token: string, report_id: int):
return requests.put(
signReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + project_manager_token},
)
def unsignReport(project_manager_token: string, report_id: int):
return requests.put(
unsignReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + project_manager_token},
)
def deleteReport(report_id: int):
return requests.delete(
deleteReportPath + "/" + str(report_id),
)
def getStatistics(token: string, projectName: string):
response = requests.get(
getStatisticsPath,
headers = {"Authorization": "Bearer " + token},
params={"projectName": projectName}
)
return response.json()
def changeProjectName(token: string, projectName: string, newProjectName: string):
response = requests.put(
changeProjectNamePath + "/" + projectName,
headers = {"Authorization": "Bearer " + token},
params={"newProjectName": newProjectName}
)
return response
def changeUserPassword(token: string, username: string, newPassword: string):
response = requests.put(
changeUserPasswordPath + "/" + username,
headers = {"Authorization": "Bearer " + token},
params={"newPassword": newPassword}
)
return response

View file

@ -1,61 +1,23 @@
import requests
import string
import random
debug_output = True
def gprint(*args, **kwargs):
print("\033[92m", *args, "\033[00m", **kwargs)
# This modules contains helper functions for the tests
from helpers import *
print("Running Tests...")
def dprint(*args, **kwargs):
if debug_output:
print(*args, **kwargs)
def randomString(len=10):
"""Generate a random string of fixed length"""
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(len))
# Defined once per test run
username = "user_" + randomString()
projectName = "project_" + randomString()
# The base URL of the API
base_url = "http://localhost:8080"
# Endpoint to test
registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/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"
getWeeklyReportsUserPath = base_url + "/api/getWeeklyReportsUser"
checkIfProjectManagerPath = base_url + "/api/checkIfProjectManager"
ProjectRoleChangePath = base_url + "/api/ProjectRoleChange"
getUsersProjectPath = base_url + "/api/getUsersProject"
getUnsignedReportsPath = base_url + "/api/getUnsignedReports"
getChangeUserNamePath = base_url + "/api/changeUserName"
getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport"
removeProjectPath = base_url + "/api/removeProject"
#ta bort auth i handlern för att få testet att gå igenom
# ta bort auth i handlern för att få testet att gå igenom
def test_ProjectRoleChange():
dprint("Testing ProjectRoleChange")
localUsername = randomString()
localProjectName = randomString()
register(localUsername, "username_password")
token = login(localUsername, "username_password").json()[
"token"
]
token = login(localUsername, "username_password").json()["token"]
# Just checking since this test is built somewhat differently than the others
assert token != None, "Login failed"
@ -83,13 +45,14 @@ def test_ProjectRoleChange():
def test_get_user_projects():
username = "user2"
password = "123"
dprint("Testing get user projects")
loginResponse = login("user2", "123")
loginResponse = login(username, password)
# Check if the user is added to the project
response = requests.get(
getUserProjectsPath,
json={"username": "user2"},
getUserProjectsPath + "/" + username,
headers={"Authorization": "Bearer " + loginResponse.json()["token"]},
)
dprint(response.text)
@ -98,26 +61,6 @@ def test_get_user_projects():
gprint("test_get_user_projects successful")
# Posts the username and password to the register endpoint
def register(username: string, password: string):
dprint("Registering with username: ", username, " and password: ", password)
response = requests.post(
registerPath, json={"username": username, "password": password}
)
dprint(response.text)
return response
# Posts the username and password to the login endpoint
def login(username: string, password: string):
dprint("Logging in with username: ", username, " and password: ", password)
response = requests.post(
loginPath, json={"username": username, "password": password}
)
dprint(response.text)
return response
# Test function to login
def test_login():
response = login(username, "always_same")
@ -133,6 +76,7 @@ def test_create_user():
assert response.status_code == 200, "Registration failed"
gprint("test_create_user successful")
# Test function to add a project
def test_add_project():
loginResponse = login(username, "always_same")
@ -146,6 +90,7 @@ def test_add_project():
assert response.status_code == 200, "Add project failed"
gprint("test_add_project successful")
# Test function to submit a report
def test_submit_report():
token = login(username, "always_same").json()["token"]
@ -167,6 +112,7 @@ def test_submit_report():
assert response.status_code == 200, "Submit report failed"
gprint("test_submit_report successful")
# Test function to get a weekly report
def test_get_weekly_report():
token = login(username, "always_same").json()["token"]
@ -194,91 +140,58 @@ def test_get_project():
# 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)
# User to create
pm_user = "user" + randomString()
pm_passwd = "password"
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")
# User to add
member_user = "member" + randomString()
member_passwd = "password"
# Create a new user to add to the project
new_user = randomString()
register(new_user, "new_user_password")
# Name of the project to be created
project_name = "project" + randomString()
# 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},
)
pm_token = register_and_login(pm_user, pm_passwd)
register(member_user, member_passwd)
dprint(response.text)
response = create_project(pm_token, project_name)
assert response.status_code == 200, "Create project failed"
# Promote the user to project manager
response = addToProject(pm_token, member_user, project_name)
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")
# Pm user
pm_username = "pm" + randomString()
pm_password = "admin_password2"
# 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)
# User to add
member_user = "member" + randomString()
member_passwd = "password"
# 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},
)
# Name of the project to be created
project_name = "project" + randomString()
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")
# Register and get the tokens for both users
pm_token = register_and_login(pm_username, pm_password)
member_token = register_and_login(member_user, member_passwd)
# Log in as the project manager
project_manager_token = login(project_manager, "project_manager_password").json()[
"token"
]
# Create the project
response = create_project(pm_token, project_name)
assert response.status_code == 200, "Create project failed"
# Add the user to the project
response = addToProject(pm_token, member_user, project_name)
# Submit a report for the project
token = login(username, "always_same").json()["token"]
response = requests.post(
submitReportPath,
json={
"projectName": projectName,
"week": 2,
response = submitReport(
member_token,
{
"projectName": project_name,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
@ -286,46 +199,99 @@ def test_sign_report():
"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"]
report_id = getReport(member_token, member_user, project_name)["reportId"]
# Sign the report as the project manager
response = requests.put(
signReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + project_manager_token},
)
response = signReport(pm_token, report_id)
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)
report_id = getReport(member_token, member_user, project_name)["reportId"]
assert report_id != None, "Get report failed"
gprint("test_sign_report successful")
# Test function to unsign a report
def test_unsign_report():
# Pm user
pm_username = "pm" + randomString()
pm_password = "admin_password2"
# User to add
member_user = "member" + randomString()
member_passwd = "password"
# Name of the project to be created
project_name = "project" + randomString()
# Register and get the tokens for both users
pm_token = register_and_login(pm_username, pm_password)
member_token = register_and_login(member_user, member_passwd)
# Create the project
response = create_project(pm_token, project_name)
assert response.status_code == 200, "Create project failed"
# Add the user to the project
response = addToProject(pm_token, member_user, project_name)
# Submit a report for the project
response = submitReport(
member_token,
{
"projectName": project_name,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
)
assert response.status_code == 200, "Submit report failed"
# Retrieve the report ID
report_id = getReport(member_token, member_user, project_name)["reportId"]
# Sign the report as the project manager
response = signReport(pm_token, report_id)
assert response.status_code == 200, "Sign report failed"
dprint("Sign report successful")
# Retrieve the report ID again for confirmation
report = getReport(member_token, member_user, project_name)
dprint(report)
report_id = report["reportId"]
assert report_id != None, "Get report failed"
# Unsign the report as the project manager
response = unsignReport(pm_token, report_id)
assert response.status_code == 200, "Unsign report failed"
dprint("Unsign report successful")
# Retrieve the report ID again for confirmation
report = getReport(member_token, member_user, project_name)
assert report_id != None, "Get report failed"
dprint(report)
assert report["signedBy"] == None, "Report was not unsigned"
gprint("test_unsign_report successful")
# Test function to get weekly reports for a user in a project
def test_get_weekly_reports_user():
def test_get_all_weekly_reports():
# Log in as the user
token = login(username, "always_same").json()["token"]
# Get weekly reports for the user in the project
response = requests.get(
getWeeklyReportsUserPath + "/" + projectName,
getAllWeeklyReportsPath + "/" + projectName,
headers={"Authorization": "Bearer " + token},
params={"targetUser": username},
)
dprint(response.text)
@ -333,7 +299,6 @@ def test_get_weekly_reports_user():
gprint("test_get_weekly_reports_user successful")
# Test function to check if a user is a project manager
def test_check_if_project_manager():
# Log in as the user
@ -349,6 +314,7 @@ def test_check_if_project_manager():
assert response.status_code == 200, "Check if project manager failed"
gprint("test_check_if_project_manager successful")
def test_ensure_manager_of_created_project():
# Create a new user to add to the project
newUser = "karen_" + randomString()
@ -372,6 +338,7 @@ def test_ensure_manager_of_created_project():
assert response.json()["isProjectManager"] == True, "User is not project manager"
gprint("test_ensure_admin_of_created_project successful")
def test_change_user_name():
# Register a new user
new_user = randomString()
@ -409,6 +376,7 @@ def test_change_user_name():
assert response.status_code == 200, "Change user name failed"
gprint("test_change_user_name successful")
def test_list_all_users_project():
# Log in as a user who is a member of the project
admin_username = randomString()
@ -437,6 +405,7 @@ def test_list_all_users_project():
assert response.status_code == 200, "List all users project failed"
gprint("test_list_all_users_project sucessful")
def test_update_weekly_report():
# Log in as the user
token = login(username, "always_same").json()["token"]
@ -502,6 +471,7 @@ def test_remove_project():
assert response.status_code == 200, "Remove project failed"
gprint("test_remove_project successful")
def test_get_unsigned_reports():
# Log in as the user
token = login("user2", "123").json()["token"]
@ -515,7 +485,266 @@ def test_get_unsigned_reports():
gprint("test_get_unsigned_reports successful")
def test_get_other_users_report_as_pm():
# Create user
user = randomString()
register(user, "password")
# Create project
project = randomString()
pm_token = login(user, "password").json()["token"]
response = requests.post(
addProjectPath,
json={"name": project, "description": "This is a project"},
headers={"Authorization": "Bearer " + pm_token},
)
assert response.status_code == 200, "Add project failed"
# Create other user
other_user = randomString()
register(other_user, "password")
user_token = login(other_user, "password").json()["token"]
# Add other user to project
response = requests.put(
addUserToProjectPath + "/" + project,
headers={"Authorization": "Bearer " + pm_token}, # note pm_token
params={"userName": other_user},
)
assert response.status_code == 200, "Add user to project failed"
# Submit report as other user
response = requests.post(
submitReportPath,
json={
"projectName": project,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
headers={"Authorization": "Bearer " + user_token},
)
assert response.status_code == 200, "Submit report failed"
# Get report as project manager
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + pm_token},
params={"targetUser": other_user, "projectName": project, "week": 1},
)
assert response.status_code == 200, "Get weekly report failed"
def test_promote_to_manager():
# User to create
pm_user = "user" + randomString()
pm_passwd = "password"
# User to promote
member_user = "member" + randomString()
member_passwd = "password"
# Name of the project to be created
project_name = "project" + randomString()
pm_token = register_and_login(pm_user, pm_passwd)
member_token = register_and_login(member_user, member_passwd)
response = create_project(pm_token, project_name)
assert response.status_code == 200, "Create project failed"
# Promote the user to project manager
response = promoteToManager(pm_token, member_user, project_name)
assert response.status_code == 200, "Promote to manager failed"
def test_delete_report():
# Create 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},
)
# Create a new project
new_project = randomString()
response = requests.post(
addProjectPath,
json={"name": new_project, "description": "This is a project"},
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Add project failed"
# Create a new report
new_report = {
"projectName": new_project,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
}
response = submitReport(admin_token, new_report);
assert response.status_code == 200, "Submit report failed"
# Get the report ID
report_id = getReport(admin_token, admin_username, new_project)["reportId"]
assert report_id != None, "Get report failed"
# Delete the report
response = requests.delete(
deleteReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Delete report failed"
# Check if the report was deleted
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + admin_token},
params={"username": admin_username, "projectName": new_project, "week": 1},
)
assert response.status_code == 500, "Report was not deleted"
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")
def test_project_name_change():
# Create admin
admin_username = randomString()
admin_password = randomString()
project_name = "project" + randomString()
token = register_and_login(admin_username, admin_password)
# Promote to admin
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + token},
)
response = create_project(token, project_name)
assert response.status_code == 200, "Create project failed"
response = requests.get(
getUserProjectsPath + "/" + admin_username,
headers={"Authorization": "Bearer " + token},
)
dprint(response.json())
new_project_name = "new project name " + randomString()
dprint("Changing project name from ", project_name, " to ", new_project_name)
response = changeProjectName(token, project_name, new_project_name)
response = requests.get(
getUserProjectsPath + "/" + admin_username,
headers={"Authorization": "Bearer " + token},
)
dprint(response.json())
if (response.json()[0]["name"] != new_project_name):
assert False, "Project name change failed"
assert response.status_code == 200, "Project name change failed"
gprint("test_projectNameChange successful")
def test_change_user_password():
# Create admin
admin_username = randomString()
admin_password = randomString()
user = randomString()
password = randomString()
token = register_and_login(admin_username, admin_password)
# Promote to admin
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + token},
)
_ = register_and_login(user, password)
response = changeUserPassword(token, user, "new_password")
assert response.status_code == 200, "Change user password failed"
response = login(user, "new_password")
assert response.status_code == 200, "Login failed with new password"
gprint("test_change_user_password successful")
if __name__ == "__main__":
test_change_user_password()
test_project_name_change();
test_delete_report()
test_unsign_report()
test_promote_to_manager()
test_remove_project()
test_get_user_projects()
test_create_user()
@ -526,7 +755,7 @@ if __name__ == "__main__":
test_get_project()
test_sign_report()
test_add_user_to_project()
test_get_weekly_reports_user()
test_get_all_weekly_reports()
test_check_if_project_manager()
test_ProjectRoleChange()
test_ensure_manager_of_created_project()
@ -534,4 +763,5 @@ if __name__ == "__main__":
test_list_all_users_project()
test_change_user_name()
test_update_weekly_report()
test_get_other_users_report_as_pm()
test_get_statistics()