Compare commits

...

201 commits

Author SHA1 Message Date
Imbus
47b60038b4 Merge branch 'frontend' into dev 2024-03-18 21:24:42 +01:00
Imbus
e0de61dd94 Type fixes in frontend, Register & YourProjectsPage 2024-03-18 21:24:26 +01:00
Imbus
f437b25da5 Merge branch 'frontend' into dev 2024-03-18 21:18:49 +01:00
al8763be
8eb23bf7f9 lint bro happ + test for getUserProject 2024-03-18 21:08:33 +01:00
Imbus
4683dd459a Remove code related to demo button in backend 2024-03-18 20:56:00 +01:00
al8763be
4392b68397 Removed duplicate getUserProjects 2024-03-18 17:40:53 +01:00
al8763be
d0cc6f2c1b Merge remote-tracking branch 'origin/dev' into BumBranch 2024-03-18 17:35:19 +01:00
al8763be
805d05f8a5 Merge branch 'gruppPP' into BumBranch 2024-03-18 17:31:03 +01:00
Imbus
fe6942aa81 Merge imbs -> dev 2024-03-18 17:30:50 +01:00
al8763be
e5904253e3 Merge branch 'gruppdm' into BumBranch 2024-03-18 17:28:57 +01:00
Peter KW
d692165f99 Removed username prop from basicwindow in all pages 2024-03-18 17:27:32 +01:00
Peter KW
388a430613 Tiny fix 2024-03-18 17:22:40 +01:00
Peter KW
2493932f77 Removed username prop, no longer used 2024-03-18 17:20:29 +01:00
al8763be
fbf46b7cd0 Merge branch 'frontend' into BumBranch 2024-03-18 17:14:23 +01:00
pavel Hamawand
b93ef48500 minor fix 2024-03-18 17:07:48 +01:00
pavel Hamawand
0f7f866cde minor fix 2024-03-18 17:06:05 +01:00
pavel Hamawand
3ec0d168eb minor fix 2024-03-18 17:05:37 +01:00
pavel Hamawand
e47b251c14 implement component 2024-03-18 17:03:02 +01:00
pavel Hamawand
39983c7f6f Render Project List 2024-03-18 17:01:11 +01:00
pavel Hamawand
f61ef87d5e Fetch Projects from API - needs fixing 2024-03-18 16:58:46 +01:00
pavel Hamawand
90afe80408 Implement State management 2024-03-18 16:54:16 +01:00
pavel Hamawand
4173003d32 initial component Setup 2024-03-18 16:53:13 +01:00
pavel Hamawand
b8f669e454 new component UserProjectListAdmin 2024-03-18 16:52:40 +01:00
pavel Hamawand
22b9580f51 fix backbutton 2024-03-18 16:52:40 +01:00
pavel Hamawand
8291f4caf3 delete component 2024-03-18 16:52:40 +01:00
pavel Hamawand
576a137038 new component 2024-03-18 16:52:40 +01:00
Peter KW
ace11570a5 Merge branch 'gruppDM' into gruppPP 2024-03-18 16:49:02 +01:00
Peter KW
2bd9878359 Merge branch 'frontend' into gruppPP 2024-03-18 16:44:13 +01:00
Imbus
0c2617d0cb Full fix for getProject route, testing, integration testing and frontend-API code 2024-03-18 16:42:35 +01:00
dDogge
9ad89d6063 Handler for SignReport added and corresponding test in testing.pu added 2024-03-18 16:01:51 +01:00
Mattias
437520183b removed import 2024-03-18 15:46:49 +01:00
Mattias
b962a856f4 fixes 2024-03-18 15:46:23 +01:00
Mattias
3bcb7a89b8 edited paths 2024-03-18 15:39:29 +01:00
Mattias
43f13dc534 Old paths in main are back 2024-03-18 15:33:19 +01:00
Mattias
1893340c63 Changed name of new component and minor fix 2024-03-18 15:17:53 +01:00
borean
3a3690e3da ignore go.work.sum 2024-03-18 15:12:11 +01:00
dDogge
4979378779 Handler for AddUserToProject and promoteToAdmin, successfully tested 2024-03-18 14:47:15 +01:00
Mattias
3106e0f445 Fixes for viewing-report-component 2024-03-18 14:46:32 +01:00
al8763be
d6ce4a3c57 errMessage fixed 2024-03-18 14:43:28 +01:00
Mattias
b82f73d192 Removed button 2024-03-18 14:42:21 +01:00
Mattias
0634f83689 Now displaying the right component 2024-03-18 14:40:32 +01:00
Mattias
69d4067209 Added new component for viewing weeklyreport 2024-03-18 14:40:20 +01:00
pavel Hamawand
409d973d8a minor fix 2024-03-18 14:35:56 +01:00
dDogge
76fefd2b24 Added function to check if someone is admin 2024-03-18 13:32:55 +01:00
Davenludd
3f8d56963b Add ProjectNameContext to NewWeeklyReport and handle project selection in YourProjectsPage 2024-03-18 12:49:58 +01:00
Davenludd
164ff781b3 Add useEffect hook to handle authority navigation and log response in YourProjectsPage 2024-03-18 10:44:15 +01:00
Davenludd
f6b2d17b97 Update Header component to use localStorage for username 2024-03-18 10:44:15 +01:00
Mattias
7a6b875aeb Token is now fetched from local storage 2024-03-18 10:28:20 +01:00
Davenludd
68b25350ef Add username to local storage and retrieve it in YourProjectsPage 2024-03-18 10:22:38 +01:00
Davenludd
3c8b7f9da2 Merge branch 'gruppPP' into gruppDM 2024-03-18 10:10:40 +01:00
Davenludd
5bef01396b Add user-specific project list and API integration 2024-03-18 10:09:12 +01:00
Mattias
711f46f960 fixed import 2024-03-18 09:56:07 +01:00
Davenludd
ba58fea1e3 Merge branch 'frontend' into gruppDM 2024-03-18 09:01:05 +01:00
Peter KW
92119dd49e Added path + fixed import 2024-03-18 01:56:04 +01:00
Peter KW
8a2152395f Small fixes 2024-03-18 01:31:58 +01:00
Peter KW
516784c6bb Merge branch 'frontend' into gruppPP 2024-03-18 00:51:34 +01:00
Peter KW
4876038613 Merge remote-tracking branch 'origin/frontend' into gruppPP 2024-03-18 00:48:21 +01:00
Peter KW
f7b8ea7d97 Path fix 2024-03-18 00:46:04 +01:00
Peter KW
bd588048dd Import fix 2024-03-18 00:45:49 +01:00
Peter KW
070e9cc1e5 Link to button 2024-03-18 00:45:22 +01:00
Peter KW
844e94ed26 Small fixes to layout and added 2024-03-18 00:44:56 +01:00
Peter KW
0044b61ac8 Uses input field component instead now 2024-03-18 00:44:11 +01:00
Peter KW
3b49faec2d Import fix 2024-03-18 00:42:22 +01:00
Peter KW
8249678488 Using local storage for token 2024-03-18 00:42:05 +01:00
Peter KW
24e7e68ea0 Logout functionality 2024-03-18 00:41:46 +01:00
Peter KW
200da51633 AddProject component 2024-03-18 00:41:18 +01:00
Peter KW
d97516e0cd Added some paths again 2024-03-18 00:40:42 +01:00
al8763be
953c06212d API Changes 2024-03-18 00:22:32 +01:00
al8763be
7cc74866fc Merge branch 'dev' into frontend 2024-03-18 00:13:04 +01:00
al8763be
b45a20c9f5 Merge branch 'dev' into frontend 2024-03-18 00:06:20 +01:00
Imbus
741ad50ccf Merge branch 'dev' of github.com:imbus64/TTime into dev 2024-03-17 23:51:52 +01:00
dDogge
f6e4603603 Added handler for SignWeeklyReport named SignReport in handlers_report_related 2024-03-17 23:31:52 +01:00
Imbus
c487b47373 Merge branch 'BumBranch' of github.com:imbus64/TTime into BumBranch 2024-03-17 23:17:18 +01:00
Imbus
9070846b0b Fixing getWeeklyReport tests 2024-03-17 23:17:06 +01:00
al8763be
ff6af1d250 Merge branch 'dev' into BumBranch 2024-03-17 23:11:49 +01:00
al8763be
9f20d46fa6 Test for getWeeklyReport 2024-03-17 22:57:19 +01:00
Imbus
e012b6ff12 Error checking in register component, redirect to login if success 2024-03-17 22:48:38 +01:00
al8763be
c3ce25236f Full implementation of getWeeklyProject 2024-03-17 22:35:51 +01:00
al8763be
6823102b44 Deleted API test 2024-03-17 22:27:21 +01:00
al8763be
06c50e7a42 Merge branch 'frontend' into BumBranch 2024-03-17 22:20:48 +01:00
al8763be
7339a69bce getWeeklyReport 2024-03-17 22:20:34 +01:00
Imbus
5696310a68 Merge branch 'dev' into frontend 2024-03-17 22:19:05 +01:00
Davenludd
ead1482e50 Merge branch 'frontend' into gruppDM 2024-03-17 21:39:19 +01:00
Davenludd
07aaab5530 Merge branch 'frontend' into gruppDM 2024-03-17 21:22:10 +01:00
dDogge
40caa2d158 Changed name to a test in db_test.go 2024-03-17 20:35:48 +01:00
dDogge
37bbbb6098 Added SignWeeklyReport function and 2 corresponding test, also small change to WeeklyReport.go 2024-03-17 20:33:53 +01:00
Imbus
e03727613d Extremely important formatting 2024-03-17 20:04:29 +01:00
dDogge
8a34fc07fa Weekly_report fixed for real this time 2024-03-17 19:58:44 +01:00
dDogge
b93df693d2 Fixed weekly_report 2024-03-17 19:56:16 +01:00
al8763be
447f2b73eb Merge branch 'gruppPP' into BumBranch 2024-03-17 19:42:01 +01:00
Peter KW
888256e9f6 Changed logincheck type and error clarification 2024-03-17 19:38:51 +01:00
Imbus
3683552af8 Verbose debug printing in register endpoint 2024-03-17 19:33:13 +01:00
al8763be
c7fe5e8775 Merge branch 'dev' into frontend 2024-03-17 19:28:57 +01:00
al8763be
402b0ac08b Update API.test.ts 2024-03-17 19:28:03 +01:00
Imbus
9240d5e052 Verbose debug printing in login endpoint 2024-03-17 19:24:13 +01:00
Peter KW
1977125923 Now exporting APIResponse interface 2024-03-17 19:18:03 +01:00
Peter KW
b999e47f94 Changes to handlesubmit 2024-03-17 19:17:34 +01:00
Peter KW
e7e79ced13 Now checks if user is in database with api.login and sets proper authority level based on name for now 2024-03-17 19:16:57 +01:00
al8763be
1a412dc8a0 Merge branch 'frontend' of https://github.com/imbus64/TTime into frontend 2024-03-17 19:08:17 +01:00
Imbus
eae252c2a4 Merge branch 'frontend' of github.com:imbus64/TTime into frontend 2024-03-17 19:07:55 +01:00
Imbus
30cf0b6065 Making linter bro happy 2024-03-17 19:07:49 +01:00
al8763be
693b5fb9ae Merge branch 'dev' into frontend 2024-03-17 19:06:14 +01:00
al8763be
14668ea675 Test file for API 2024-03-17 19:05:39 +01:00
dDogge
3e00a532cf Added handler for GetWeeklyReport 2024-03-17 18:05:54 +01:00
dDogge
a77e57e496 Added GetWeeklyReport function and corresponding test 2024-03-17 17:58:02 +01:00
dDogge
c90d495636 Added new types and changed SQL since apperently sqlite does use autoincrement 2024-03-17 17:47:31 +01:00
Imbus
ec46d29423 Merge branch 'imbs' into dev 2024-03-17 16:56:00 +01:00
Mattias
17a081e816 minor fixes 2024-03-17 16:55:51 +01:00
Imbus
d6fc0594a9 Splitting backend endpoints into smaller bits 2024-03-17 16:55:40 +01:00
Imbus
1be3ee89c2 Merge branch 'dev' of github.com:imbus64/TTime into dev 2024-03-17 16:42:02 +01:00
Imbus
ec36054bd9 Merge branch 'imbs' into dev 2024-03-17 16:41:51 +01:00
Imbus
670ed46d51 WeeklyReport type as represented in the db 2024-03-17 16:41:29 +01:00
Mattias
81893ae3e8 Minor fixes 2024-03-17 16:24:09 +01:00
Davenludd
9dd47f3d71 Update import paths for Types 2024-03-17 16:23:37 +01:00
pavel Hamawand
141e5c8bb6 minor fix 2024-03-17 16:21:36 +01:00
Davenludd
2ba10e837a Refactor API.ts to handle error response in submitWeeklyReport 2024-03-17 16:14:50 +01:00
al8763be
d57fe55074 Changes to SubmitWeeklyReport 2024-03-17 16:11:03 +01:00
Peter KW
b7fcafd75c Merge branch 'frontend' into gruppPP 2024-03-17 15:54:06 +01:00
Davenludd
830c234325 Update login response type in API.ts 2024-03-17 15:50:50 +01:00
al8763be
386f7c3a94 API Login return { success: true, data: data.token } 2024-03-17 15:49:34 +01:00
al8763be
d11e5e64f6 API Login return { success: true, data } data as JSON with token 2024-03-17 15:41:23 +01:00
Peter KW
060dc1ee3d Merge branch 'frontend' into gruppPP 2024-03-17 15:39:16 +01:00
Davenludd
c708ec47ca Merge branch 'frontend' into gruppDM 2024-03-17 15:36:41 +01:00
al8763be
a28bab459a API login returns { success: true, data: token } 2024-03-17 15:36:39 +01:00
dDogge
21cc7ff8a3 Added signed_by attribute to weekly_report SQL table 2024-03-17 15:34:48 +01:00
al8763be
9511d509ca API imports fixed 2024-03-17 15:31:15 +01:00
Davenludd
b1f57e5ed8 Merge branch 'frontend' into gruppDM 2024-03-17 15:28:20 +01:00
al8763be
f5787fbc2e correct routes in main 2024-03-17 15:23:21 +01:00
al8763be
179a5196e9 Merge branch 'BumBranch' into frontend 2024-03-17 15:10:19 +01:00
al8763be
5327ac7ad1 lint + login function for API 2024-03-17 15:03:34 +01:00
Imbus
ff00b984fe Formatting and typing to make linter happy v2 2024-03-17 14:52:13 +01:00
Davenludd
6d5c25be5c Refactor Register component to use InputField component 2024-03-17 14:49:42 +01:00
Mattias
d6d2b6d170 Visual fixes 2024-03-17 14:49:42 +01:00
Imbus
cab7c3bdfd Formatting and typing to make linter happy 2024-03-17 14:44:25 +01:00
al8763be
2eecf17d42 Login feature API 2024-03-17 14:43:09 +01:00
Imbus
58b74e3f04 Merge branch 'dev' into frontend 2024-03-17 14:38:44 +01:00
Imbus
7c21677310 Migrations fix in go 2024-03-17 14:38:20 +01:00
Davenludd
46eebee84f Refactor API.ts for improved readability and maintainability 2024-03-17 14:21:38 +01:00
Mattias
f3931f905a Minor changes 2024-03-17 14:19:57 +01:00
pavel Hamawand
79eb59ad46 button fix 2024-03-17 14:13:35 +01:00
Mattias
62f1926305 Corrected NewWeeklyReport (prev. TimeReports) and changed imports 2024-03-17 13:39:16 +01:00
Peter KW
9bab627255 Change typ on button to submit 2024-03-17 12:57:18 +01:00
al8763be
8d0da111eb API - SubmitWeeklyReport & getUserProjects added 2024-03-17 12:35:53 +01:00
Peter KW
b6535c883d Small fix 2024-03-17 11:34:39 +01:00
Peter KW
3e73b11698 Merge branch 'frontend' into gruppPP 2024-03-17 11:02:44 +01:00
al8763be
d6d4eb3336 Merge branch 'master' into gruppDM 2024-03-17 10:51:23 +01:00
Peter KW
5e0b96ed87 Merge branch 'frontend' into gruppPP 2024-03-17 10:43:49 +01:00
Peter KW
8cf873a98b Changed so that most functionality was moved to separate components 2024-03-17 10:39:57 +01:00
Peter KW
15ec2108d3 Everything run from this file 2024-03-17 10:38:25 +01:00
Peter KW
02332c284b Login field component 2024-03-17 10:36:29 +01:00
Peter KW
94f5d3f85b Component for checking login, to be replaced with proper fetching 2024-03-17 10:35:48 +01:00
Peter KW
217b7b4a22 Input field component 2024-03-17 10:34:47 +01:00
Peter KW
2921458d82 Made backround animation on login page to its own component 2024-03-17 10:34:15 +01:00
Peter KW
c12e19770e Restructuring, less paths 2024-03-17 10:33:37 +01:00
Imbus
7f5270f536 Merge branch 'gruppDM' of github.com:imbus64/TTime into gruppDM 2024-03-16 18:38:50 +01:00
Davenludd
d58720cdbd Refactor Register component to use InputField component 2024-03-16 18:38:42 +01:00
Mattias
92a4808cdd Created new type for timereport 2024-03-16 18:38:42 +01:00
Mattias
f3c5ce57eb Submit-button has been moved to the timereport 2024-03-16 18:38:42 +01:00
Mattias
3419898c66 Created groundwork for the timereport 2024-03-16 18:38:42 +01:00
Mattias
aae05acedb Added the register component to content 2024-03-16 18:38:42 +01:00
Mattias
a225020f6f Visual fixes 2024-03-16 18:38:42 +01:00
Mattias
2ce480707a Removed duplicate path 2024-03-16 18:38:42 +01:00
pavel Hamawand
17edf6b911 backButton 2024-03-16 18:01:14 +01:00
pavel Hamawand
31e3b99448 backbutton 2024-03-16 18:00:10 +01:00
pavel Hamawand
38378c7a10 babkbutton 2024-03-16 17:59:25 +01:00
pavel Hamawand
92336ff4fd backbutton 2024-03-16 17:58:27 +01:00
pavel Hamawand
17e9fa9d0a backbutton 2024-03-16 17:57:51 +01:00
pavel Hamawand
bdd6f68470 Time / role link 2024-03-16 17:56:59 +01:00
pavel Hamawand
9aa5087871 time / activity link 2024-03-16 17:54:34 +01:00
pavel Hamawand
8164ba9040 fix back button 2024-03-16 17:50:15 +01:00
pavel Hamawand
49305d507f link to projectMembers 2024-03-16 17:48:23 +01:00
pavel Hamawand
e837f6e746 link to your time Reports 2024-03-16 17:37:09 +01:00
pavel Hamawand
a6846ad7a4 fix backbutton 2024-03-16 17:28:45 +01:00
pavel Hamawand
dc84bc997e link to unsigned reports 2024-03-16 17:25:43 +01:00
pavel Hamawand
554cf9b12e remove backbutton 2024-03-16 17:20:14 +01:00
Davenludd
7fb220f768 Refactor Register component to use InputField component 2024-03-16 15:14:03 +01:00
Mattias
d71752ad6f Created new type for timereport 2024-03-16 13:11:39 +01:00
Mattias
87c044b5bf Submit-button has been moved to the timereport 2024-03-16 13:11:26 +01:00
Mattias
966f8540df Created groundwork for the timereport 2024-03-16 13:10:46 +01:00
Mattias
d227ffc6ae Added the register component to content 2024-03-16 02:47:39 +01:00
Mattias
a67c44564f Visual fixes 2024-03-16 02:47:17 +01:00
Mattias
148af4e499 Removed duplicate path 2024-03-16 02:46:58 +01:00
Peter KW
9ff3e9314c Small change to header; uses useLocation() to get correct name 2024-03-16 02:27:33 +01:00
Peter KW
edf503776a Changed design a little so that it uses the projectlist component, added some fake projects 2024-03-16 02:23:52 +01:00
Peter KW
4a864e1ab5 Finished design and added fake users for testing 2024-03-16 02:20:36 +01:00
Peter KW
8fdd6c0b1f Finished basic view for this page 2024-03-16 02:18:28 +01:00
Peter KW
466c25a7c2 A component for listing an array of users where every user gets a link, made for AdminManageUsers.tsx 2024-03-16 02:16:06 +01:00
Peter KW
f963ca6ae5 A component for listing an array of projects where every project gets a link, made for YourProjectsPage.tsx 2024-03-16 02:15:39 +01:00
Peter KW
f0fc465d1a Merge remote-tracking branch 'origin/frontend' into gruppPP 2024-03-15 16:54:26 +01:00
pavel Hamawand
2da40e1f54 change back button UserViewTimeReportsPage 2024-03-15 15:32:06 +01:00
pavel Hamawand
8430d88a07 change back button UserProjectPage 2024-03-15 15:28:35 +01:00
pavel Hamawand
a26499bde9 change backbutton UserEditTimeReport and UserNewTimeReport 2024-03-15 15:22:55 +01:00
Peter KW
6789cc97ce Added back button to page 2024-03-15 14:57:49 +01:00
Peter KW
44aea9b765 Made a back button component since we will have one on almost every page 2024-03-15 14:51:46 +01:00
pavel Hamawand
69b0318eb0 remove edit button and fix back button 2024-03-15 14:48:40 +01:00
pavel Hamawand
dc902855f4 add edit button to viewTimeReport 2024-03-15 04:42:38 +01:00
pavel Hamawand
901bcd39c5 link to viewTimeReport 2024-03-15 04:42:38 +01:00
Peter KW
cd1dbb494c Small fix to links in admin-menu page 2024-03-15 02:29:25 +01:00
Peter KW
2ad7146588 Added some functionality to login page. Checks username + password and compares with "fake" users to determine which page to get 2024-03-15 02:28:28 +01:00
pavel Hamawand
ef28e1743e links to all projects 2024-03-15 02:03:22 +01:00
pavel Hamawand
92cf36d178 increased responsiveness - outer div 2024-03-15 01:42:43 +01:00
61 changed files with 2300 additions and 775 deletions

1
.gitignore vendored
View file

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

View file

@ -2,7 +2,7 @@ package database
import ( import (
"embed" "embed"
"os" "errors"
"path/filepath" "path/filepath"
"ttime/internal/types" "ttime/internal/types"
@ -19,7 +19,7 @@ type Database interface {
PromoteToAdmin(username string) error PromoteToAdmin(username string) error
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
Migrate(dirname string) error Migrate() error
GetProjectId(projectname string) (int, error) GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
@ -30,6 +30,9 @@ type Database interface {
GetAllProjects() ([]types.Project, error) GetAllProjects() ([]types.Project, error)
GetProject(projectId int) (types.Project, error) GetProject(projectId int) (types.Project, error)
GetUserRole(username string, projectname string) (string, error) GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error)
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
@ -104,7 +107,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) {
// GetProject retrieves a specific project by its ID. // GetProject retrieves a specific project by its ID.
func (d *Db) GetProject(projectId int) (types.Project, error) { func (d *Db) GetProject(projectId int) (types.Project, error) {
var project types.Project var project types.Project
err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId)
if err != nil {
println("Error getting project: ", err)
}
return project, err return project, err
} }
@ -257,15 +263,94 @@ func (d *Db) GetAllUsersApplication() ([]string, error) {
return usernames, nil return usernames, nil
} }
func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) {
var report types.WeeklyReport
query := `
SELECT
report_id,
user_id,
project_id,
week,
development_time,
meeting_time,
admin_time,
own_work_time,
study_time,
testing_time,
signed_by
FROM
weekly_reports
WHERE
user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
AND week = ?
`
err := d.Get(&report, query, username, projectName, week)
return report, err
}
// SignWeeklyReport signs a weekly report by updating the signed_by field
// with the provided project manager's ID, but only if the project manager
// is in the same project as the report
func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
if err != nil {
return err
}
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors.New("project manager doesn't have permission to sign the report")
}
// Update the signed_by field of the specified report
_, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId)
return err
}
// IsSiteAdmin checks if a given username is a site admin
func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Define the SQL query to check if the user is a site admin
query := `
SELECT COUNT(*) FROM site_admin
JOIN users ON site_admin.admin_id = users.id
WHERE users.username = ?
`
// Execute the query
var count int
err := d.Get(&count, query, username)
if err != nil {
return false, err
}
// If count is greater than 0, the user is a site admin
return count > 0, nil
}
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func (d *Db) Migrate(dirname string) error { func (d *Db) Migrate() error {
// Read the embedded scripts directory // Read the embedded scripts directory
files, err := scripts.ReadDir("migrations") files, err := scripts.ReadDir("migrations")
if err != nil { if err != nil {
return err return err
} }
if len(files) == 0 {
println("No migration files found")
return nil
}
tr := d.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
@ -275,8 +360,7 @@ func (d *Db) Migrate(dirname string) error {
} }
// This is perhaps not the most elegant way to do this // This is perhaps not the most elegant way to do this
sqlFile := filepath.Join("migrations", file.Name()) sqlBytes, err := scripts.ReadFile("migrations/" + file.Name())
sqlBytes, err := os.ReadFile(sqlFile)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,6 +1,7 @@
package database package database
import ( import (
"fmt"
"testing" "testing"
) )
@ -8,7 +9,7 @@ import (
func setupState() (Database, error) { func setupState() (Database, error) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
err := db.Migrate("../../migrations") err := db.Migrate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -371,3 +372,197 @@ func TestAddProject(t *testing.T) {
t.Error("Added project not found") t.Error("Added project not found")
} }
} }
func TestGetWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Check if the retrieved report matches the expected values
if report.UserId != 1 {
t.Errorf("Expected UserId to be 1, got %d", report.UserId)
}
if report.ProjectId != 1 {
t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId)
}
if report.Week != 1 {
t.Errorf("Expected Week to be 1, got %d", report.Week)
}
// Check other fields similarly
}
func TestSignWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add both regular users as members to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
err = db.AddUserToProject("projectManager", "testproject", "project_manager")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for one of the regular users
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Print project manager's ID
projectManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
fmt.Println("Project Manager's ID:", projectManagerID)
// Sign the report with the project manager
err = db.SignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("SignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's signed
signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is signed by the project manager
if *signedReport.SignedBy != projectManagerID {
t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy)
}
}
func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add the regular user as a member to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for the regular user
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
anotherManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
err = db.SignWeeklyReport(report.ReportId, anotherManagerID)
if err == nil {
t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't")
}
}
func TestGetProject(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Retrieve the added project
project, err := db.GetProject(1)
if err != nil {
t.Error("GetProject failed:", err)
}
// Check if the retrieved project matches the expected values
if project.Name != "testproject" {
t.Errorf("Expected Name to be testproject, got %s", project.Name)
}
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,9 @@
package handlers package handlers
import ( import (
"strconv"
"time"
"ttime/internal/database" "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
// The actual interface that we will use // The actual interface that we will use
@ -19,6 +15,11 @@ type GlobalState interface {
CreateProject(c *fiber.Ctx) error // To create a new project CreateProject(c *fiber.Ctx) error // To create a new project
GetUserProjects(c *fiber.Ctx) error // To get all projects GetUserProjects(c *fiber.Ctx) error // To get all projects
SubmitWeeklyReport(c *fiber.Ctx) error SubmitWeeklyReport(c *fiber.Ctx) error
GetWeeklyReport(c *fiber.Ctx) error
SignReport(c *fiber.Ctx) error
GetProject(c *fiber.Ctx) error
AddUserToProjectHandler(c *fiber.Ctx) error
PromoteToAdmin(c *fiber.Ctx) error
// GetProject(c *fiber.Ctx) error // To get a specific project // GetProject(c *fiber.Ctx) error // To get a specific project
// UpdateProject(c *fiber.Ctx) error // To update a project // UpdateProject(c *fiber.Ctx) error // To update a project
// DeleteProject(c *fiber.Ctx) error // To delete a project // DeleteProject(c *fiber.Ctx) error // To delete a project
@ -33,8 +34,6 @@ type GlobalState interface {
// UpdateCollection(c *fiber.Ctx) error // To update a collection // UpdateCollection(c *fiber.Ctx) error // To update a collection
// DeleteCollection(c *fiber.Ctx) error // To delete a collection // DeleteCollection(c *fiber.Ctx) error // To delete a collection
// SignCollection(c *fiber.Ctx) error // To sign a collection // SignCollection(c *fiber.Ctx) error // To sign a collection
GetButtonCount(c *fiber.Ctx) error // For demonstration purposes
IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes
ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database
ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project
ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project
@ -42,242 +41,10 @@ type GlobalState interface {
// "Constructor" // "Constructor"
func NewGlobalState(db database.Database) GlobalState { func NewGlobalState(db database.Database) GlobalState {
return &GState{Db: db, ButtonCount: 0} return &GState{Db: db}
} }
// The global state, which implements all the handlers // The global state, which implements all the handlers
type GState struct { type GState struct {
Db database.Database Db database.Database
ButtonCount int
}
// Register is a simple handler that registers a new user
//
// @Summary Register a new user
// @Description Register a new user
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /api/register [post]
func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
return c.Status(400).SendString(err.Error())
}
if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User added")
}
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")
// Read username from Locals
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username != auth_username {
return c.Status(403).SendString("You can only delete yourself")
}
if err := gs.Db.RemoveUser(username); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User deleted")
}
func (gs *GState) GetButtonCount(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
}
func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error {
gs.ButtonCount++
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
}
// Login is a simple login handler that returns a JWT token
func (gs *GState) Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
return c.Status(400).SendString(err.Error())
}
if !gs.Db.CheckUser(u.Username, u.Password) {
println("User not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": u.Username,
"admin": false,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// LoginRenew is a simple handler that renews the token
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// CreateProject is a simple handler that creates a new project
func (gs *GState) CreateProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
p := new(types.NewProject)
if err := c.BodyParser(p); err != nil {
return c.Status(400).SendString(err.Error())
}
// Get the username from the token and set it as the owner of the project
// This is ugly but
claims := user.Claims.(jwt.MapClaims)
owner := claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}
// GetUserProjects returns all projects that the user is a member of
func (gs *GState) GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Then dip into the database to get the projects
projects, err := gs.Db.GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}
// ListAllUsers is a handler that returns a list of all users in the application database
func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := gs.Db.GetAllUsersApplication()
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}
// ProjectRoleChange is a handler that changes a user's role within a project
func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
username := c.Params("username")
projectName := c.Params("projectName")
role := c.Params("role")
// Change the user's role within the project in the database
if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}
// GetProject retrieves a specific project by its ID
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
return c.JSON(project)
}
func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil {
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 {
return c.Status(400).SendString("Invalid week number")
}
if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 {
return c.Status(400).SendString("Invalid time report")
}
if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Time report added")
} }

View file

@ -0,0 +1,145 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// CreateProject is a simple handler that creates a new project
func (gs *GState) CreateProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
p := new(types.NewProject)
if err := c.BodyParser(p); err != nil {
return c.Status(400).SendString(err.Error())
}
// Get the username from the token and set it as the owner of the project
// This is ugly but
claims := user.Claims.(jwt.MapClaims)
owner := claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}
// GetUserProjects returns all projects that the user is a member of
func (gs *GState) GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Then dip into the database to get the projects
projects, err := gs.Db.GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}
// ProjectRoleChange is a handler that changes a user's role within a project
func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
username := c.Params("username")
projectName := c.Params("projectName")
role := c.Params("role")
// Change the user's role within the project in the database
if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}
// GetProject retrieves a specific project by its ID
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
if projectID == "" {
return c.Status(400).SendString("No project ID provided")
}
println("Getting project with ID: ", projectID)
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
println("Returning project: ", project.Name)
return c.JSON(project)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
// Extract necessary parameters from the request
var requestData struct {
Username string `json:"username"`
ProjectName string `json:"projectName"`
Role string `json:"role"`
}
if err := c.BodyParser(&requestData); err != nil {
println("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)
println("Admin username from claims:", adminUsername)
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if err != nil {
println("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
println("User is not a site admin:", adminUsername)
return c.Status(403).SendString("User is not a site admin")
}
// Add the user to the project with the specified role
err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role)
if err != nil {
println("Error adding user to project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
println("User added to project successfully:", requestData.Username)
return c.SendStatus(fiber.StatusOK)
}

View file

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

View file

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

View file

@ -19,3 +19,28 @@ type NewWeeklyReport struct {
// Total time spent on testing // Total time spent on testing
TestingTime int `json:"testingTime"` TestingTime int `json:"testingTime"`
} }
type WeeklyReport struct {
// The ID of the report
ReportId int `json:"reportId" db:"report_id"`
// The user id of the user who submitted the report
UserId int `json:"userId" db:"user_id"`
// The name of the project, as it appears in the database
ProjectId int `json:"projectId" db:"project_id"`
// The week number
Week int `json:"week" db:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime" db:"development_time"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime" db:"meeting_time"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime" db:"admin_time"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"`
// Total time spent on studying
StudyTime int `json:"studyTime" db:"study_time"`
// Total time spent on testing
TestingTime int `json:"testingTime" db:"testing_time"`
// The project manager who signed it
SignedBy *int `json:"signedBy" db:"signed_by"`
}

View file

@ -43,6 +43,11 @@ func main() {
// Connect to the database // Connect to the database
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database
if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err)
}
// Get our global state // Get our global state
gs := handlers.NewGlobalState(db) gs := handlers.NewGlobalState(db)
// Create the server // Create the server
@ -56,11 +61,6 @@ func main() {
// Register our unprotected routes // Register our unprotected routes
server.Post("/api/register", gs.Register) server.Post("/api/register", gs.Register)
// Register handlers for example button count
server.Get("/api/button", gs.GetButtonCount)
server.Post("/api/button", gs.IncrementButtonCount)
server.Post("/api/login", gs.Login) server.Post("/api/login", gs.Login)
// Every route from here on will require a valid JWT // Every route from here on will require a valid JWT
@ -68,11 +68,17 @@ func main() {
SigningKey: jwtware.SigningKey{Key: []byte("secret")}, SigningKey: jwtware.SigningKey{Key: []byte("secret")},
})) }))
// Protected routes (require a valid JWT bearer token authentication header)
server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Post("/api/submitReport", gs.SubmitWeeklyReport)
server.Get("/api/getUserProjects", gs.GetUserProjects) server.Get("/api/getUserProjects", gs.GetUserProjects)
server.Post("/api/loginrenew", gs.LoginRenew) server.Post("/api/loginrenew", gs.LoginRenew)
server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches
server.Post("/api/project", gs.CreateProject) server.Post("/api/project", gs.CreateProject)
server.Get("/api/project/:projectId", gs.GetProject)
server.Get("/api/getWeeklyReport", gs.GetWeeklyReport)
server.Post("/api/signReport", gs.SignReport)
server.Put("/api/addUserToProject", gs.AddUserToProjectHandler)
server.Post("/api/promoteToAdmin", gs.PromoteToAdmin)
// Announce the port we are listening on and start the server // Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port)) err = server.Listen(fmt.Sprintf(":%d", conf.Port))

View file

@ -1,8 +1,13 @@
import { NewProject, Project } from "../Types/Project"; import {
import { NewUser, User } from "../Types/Users"; NewWeeklyReport,
NewUser,
User,
Project,
NewProject,
} from "../Types/goTypes";
// This type of pattern should be hard to misuse // This type of pattern should be hard to misuse
interface APIResponse<T> { export interface APIResponse<T> {
success: boolean; success: boolean;
message?: string; message?: string;
data?: T; data?: T;
@ -15,13 +20,34 @@ interface API {
registerUser(user: NewUser): Promise<APIResponse<User>>; registerUser(user: NewUser): Promise<APIResponse<User>>;
/** Remove a user */ /** Remove a user */
removeUser(username: string, token: string): Promise<APIResponse<User>>; removeUser(username: string, token: string): Promise<APIResponse<User>>;
/** Login */
login(NewUser: NewUser): Promise<APIResponse<string>>;
/** Renew the token */
renewToken(token: string): Promise<APIResponse<string>>;
/** Create a project */ /** Create a project */
createProject( createProject(
project: NewProject, project: NewProject,
token: string, token: string,
): Promise<APIResponse<Project>>; ): Promise<APIResponse<Project>>;
/** Renew the token */ /** Submit a weekly report */
renewToken(token: string): Promise<APIResponse<string>>; submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<NewWeeklyReport>>;
/**Gets a weekly report*/
getWeeklyReport(
username: string,
projectName: string,
week: string,
token: string,
): Promise<APIResponse<NewWeeklyReport>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Gets a project from id*/
getProject(id: number): Promise<APIResponse<Project>>;
} }
// Export an instance of the API // Export an instance of the API
@ -37,13 +63,19 @@ export const api: API = {
}); });
if (!response.ok) { if (!response.ok) {
return { success: false, message: "Failed to register user" }; return {
success: false,
message: "Failed to register user: " + response.status,
};
} else { } else {
const data = (await response.json()) as User; // const data = (await response.json()) as User; // The API does not currently return the user
return { success: true, data }; return { success: true };
} }
} catch (e) { } catch (e) {
return { success: false, message: "Failed to register user" }; return {
success: false,
message: "Unknown error while registering user",
};
} }
}, },
@ -117,4 +149,140 @@ export const api: API = {
return { success: false, message: "Failed to renew token" }; return { success: false, message: "Failed to renew token" };
} }
}, },
async getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>> {
try {
const response = await fetch("/api/getUserProjects", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ username }),
});
if (!response.ok) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
} else {
const data = (await response.json()) as Project[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
}
},
async submitWeeklyReport(
weeklyReport: NewWeeklyReport,
token: string,
): Promise<APIResponse<NewWeeklyReport>> {
try {
const response = await fetch("/api/submitWeeklyReport", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(weeklyReport),
});
if (!response.ok) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
const data = (await response.json()) as NewWeeklyReport;
return { success: true, data };
} catch (e) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
},
async getWeeklyReport(
username: string,
projectName: string,
week: string,
token: string,
): Promise<APIResponse<NewWeeklyReport>> {
try {
const response = await fetch("/api/getWeeklyReport", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ username, projectName, week }),
});
if (!response.ok) {
return { success: false, message: "Failed to get weekly report" };
} else {
const data = (await response.json()) as NewWeeklyReport;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get weekly report" };
}
},
async login(NewUser: NewUser): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(NewUser),
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
} else {
const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token };
}
} catch (e) {
return Promise.resolve({ success: false, message: "Failed to login" });
}
},
// Gets a projet by id, currently untested since we have no javascript-based tests
async getProject(id: number): Promise<APIResponse<Project>> {
try {
const response = await fetch(`/api/project/${id}`, {
method: "GET",
});
if (!response.ok) {
return {
success: false,
message: "Failed to get project: Response code " + response.status,
};
} else {
const data = (await response.json()) as Project;
return { success: true, data };
}
// The code below is garbage but satisfies the linter
// This needs fixing, do not copy this pattern
} catch (e: unknown) {
return {
success: false,
message: "Failed to get project: " + (e as Error).toString(),
};
}
},
}; };

View file

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

View file

@ -0,0 +1,18 @@
import { useNavigate } from "react-router-dom";
function BackButton(): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(-1);
};
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;"
>
Back
</button>
);
}
export default BackButton;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,35 @@
import { Link } from "react-router-dom";
import { Project } from "../Types/goTypes";
/**
* The props for the ProjectsProps component
*/
interface ProjectProps {
projects: Project[];
}
/**
* A list of projects for users, that links the user to the right project page
* thanks to the state property
* @param props - The projects to display
* @returns {JSX.Element} The project list
* @example
* const projects = [{ id: 1, name: "Random name" }];
* return <ProjectList projects={projects} />;
*/
export function ProjectListUser(props: ProjectProps): JSX.Element {
return (
<div>
<ul className="font-bold underline text-[30px] cursor-pointer">
{props.projects.map((project) => (
<Link to="/project" key={project.id} state={project.name}>
<li className="pt-5" key={project.id}>
{project.name}
</li>
</Link>
))}
</ul>
</div>
);
}

View file

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

View file

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

View file

@ -0,0 +1,35 @@
import { Link } from "react-router-dom";
import { PublicUser } from "../Types/goTypes";
/**
* The props for the UserProps component
*/
interface UserProps {
users: PublicUser[];
}
/**
* A list of users for admin manage users page, that links admin to the right user page
* thanks to the state property
* @param props - The users to display
* @returns {JSX.Element} The user list
* @example
* const users = [{ id: 1, userName: "Random name" }];
* return <UserList users={users} />;
*/
export function UserListAdmin(props: UserProps): JSX.Element {
return (
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => (
<Link to="/adminUserInfo" key={user.userId} state={user.username}>
<li className="pt-5" key={user.userId}>
{user.username}
</li>
</Link>
))}
</ul>
</div>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,36 +1,32 @@
import Button from "../Components/Button";
import Logo from "/src/assets/Logo.svg"; import Logo from "/src/assets/Logo.svg";
import "./LoginPage.css"; import "./LoginPage.css";
import { useEffect } from "react"; import { Dispatch, FormEvent, SetStateAction, useState } from "react";
import { Link } from "react-router-dom"; import BackgroundAnimation from "../Components/BackgroundAnimation";
import LoginField from "../Components/LoginField";
import LoginCheck from "../Components/LoginCheck";
const PreloadBackgroundAnimation = (): JSX.Element => { function LoginPage(props: {
useEffect(() => { setAuthority: Dispatch<SetStateAction<number>>;
const images = [ }): JSX.Element {
"src/assets/1.jpg", const [username, setUsername] = useState("");
"src/assets/2.jpg", const [password, setPassword] = useState("");
"src/assets/3.jpg",
"src/assets/4.jpg",
];
// Pre-load images /* On submit (enter or button click) check if username and password match any user
for (const i of images) { and if so, redirect to correct page */
console.log(i); function handleSubmit(event: FormEvent<HTMLFormElement>): void {
event.preventDefault();
LoginCheck({
username: username,
password: password,
setAuthority: props.setAuthority,
});
} }
// Start animation
document.body.style.animation = "backgroundTransition 30s infinite";
}, []);
return <></>;
};
function LoginPage(): JSX.Element {
return ( return (
<> <>
<PreloadBackgroundAnimation /> <BackgroundAnimation />
<div <div
className="flex flex-col h-screen w-screen items-center justify-center" className="flex flex-col h-screen items-center justify-center bg-cover bg-fixed"
style={{ style={{
animation: "backgroundTransition 30s infinite", animation: "backgroundTransition 30s infinite",
backgroundSize: "cover", backgroundSize: "cover",
@ -51,34 +47,13 @@ function LoginPage(): JSX.Element {
{" "} {" "}
Please log in to continue{" "} Please log in to continue{" "}
</h2> </h2>
<input <LoginField
className="border-2 border-black mb-3 rounded-lg w-[20vw] p-1" handleSubmit={handleSubmit}
type="text" setUsername={setUsername}
placeholder="Username" setPassword={setPassword}
username={username}
password={password}
/> />
<input
className="border-2 border-black mb-3 rounded-lg w-[20vw] p-1"
type="password"
placeholder="Password"
/>
<Link to="/your-projects">
<Button
text="Login"
onClick={(): void => {
return;
}}
type="button"
/>
</Link>
<Link to="/register">
<Button
text="Register new user"
onClick={(): void => {
return;
}}
type="button"
/>
</Link>
</div> </div>
</div> </div>
</> </>

View file

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

View file

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

View file

@ -1,35 +1,35 @@
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import BackButton from "../../Components/BackButton";
import { Link } from "react-router-dom";
function PMProjectMembers(): JSX.Element { function PMProjectMembers(): JSX.Element {
const content = <></>; const content = <></>;
const buttons = ( const buttons = (
<> <>
<Link to="/PM-time-activity">
<Button <Button
text="Time / Activity" text="Time / Activity"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button" type={"button"}
/> />
</Link>
<Link to="/PM-time-role">
<Button <Button
text="Time / Role" text="Time / Role"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button" type={"button"}
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/> />
</Link>
<BackButton />
</> </>
); );
return <BasicWindow username="Admin" content={content} buttons={buttons} />; return <BasicWindow content={content} buttons={buttons} />;
} }
export default PMProjectMembers; export default PMProjectMembers;

View file

@ -1,39 +1,36 @@
import { Link } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import { JSX } from "react/jsx-runtime";
function PMProjectPage(): JSX.Element { function PMProjectPage(): JSX.Element {
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">ProjectNameExample</h1> <h1 className="font-bold text-[30px] mb-[20px]">ProjectNameExample</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]"> <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="/project-page">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports Your Time Reports
</h1> </h1>
</Link>
<Link to="/new-time-report">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report New Time Report
</h1> </h1>
</Link>
<Link to="/project-members">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Statistics Statistics
</h1> </h1>
</Link>
<Link to="/PM-unsigned-reports">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Unsigned Time Reports Unsigned Time Reports
</h1> </h1>
</Link>
</div> </div>
</> </>
); );
const buttons = ( return <BasicWindow content={content} buttons={undefined} />;
<>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
} }
export default PMProjectPage; export default PMProjectPage;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,17 @@
import { Link } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import BackButton from "../../Components/BackButton";
function UserProjectPage(): JSX.Element { function UserProjectPage(): JSX.Element {
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">ProjectNameExample</h1> <h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<Link to="/project-page">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports Your Time Reports
</h1> </h1>
</Link>
<Link to="/new-time-report"> <Link to="/new-time-report">
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report New Time Report
@ -21,18 +23,10 @@ function UserProjectPage(): JSX.Element {
const buttons = ( const buttons = (
<> <>
<Link to="/your-projects"> <BackButton />
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</Link>
</> </>
); );
return <BasicWindow username="Admin" content={content} buttons={buttons} />; return <BasicWindow content={content} buttons={buttons} />;
} }
export default UserProjectPage; export default UserProjectPage;

View file

@ -1,21 +1,20 @@
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import BackButton from "../../Components/BackButton";
function UserViewTimeReportsPage(): JSX.Element { function UserViewTimeReportsPage(): JSX.Element {
const content = <></>; const content = (
const buttons = (
<> <>
<Button <h1 className="font-bold text-[30px] mb-[20px]">Your Time Reports</h1>
text="Back" {/* Här kan du inkludera logiken för att visa användarens tidrapporter */}
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );
return <BasicWindow username="Admin" content={content} buttons={buttons} />; const buttons = (
<>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
} }
export default UserViewTimeReportsPage; export default UserViewTimeReportsPage;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,12 @@ registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login" loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project" addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/submitReport" submitReportPath = base_url + "/api/submitReport"
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"
# Posts the username and password to the register endpoint # Posts the username and password to the register endpoint
@ -42,20 +48,20 @@ def login(username: string, password: string):
print(response.text) print(response.text)
return response return response
# Test function to login
def test_login(): def test_login():
response = login(username, "always_same") response = login(username, "always_same")
assert response.status_code == 200, "Login failed" assert response.status_code == 200, "Login failed"
print("Login successful") print("Login successful")
return response.json()["token"] return response.json()["token"]
# Test function to create a new user
def test_create_user(): def test_create_user():
response = register(username, "always_same") response = register(username, "always_same")
assert response.status_code == 200, "Registration failed" assert response.status_code == 200, "Registration failed"
print("Registration successful") print("Registration successful")
# Test function to add a project
def test_add_project(): def test_add_project():
loginResponse = login(username, "always_same") loginResponse = login(username, "always_same")
token = loginResponse.json()["token"] token = loginResponse.json()["token"]
@ -68,13 +74,13 @@ def test_add_project():
assert response.status_code == 200, "Add project failed" assert response.status_code == 200, "Add project failed"
print("Add project successful") print("Add project successful")
# Test function to submit a report
def test_submit_report(): def test_submit_report():
token = login(username, "always_same").json()["token"] token = login(username, "always_same").json()["token"]
response = requests.post( response = requests.post(
submitReportPath, submitReportPath,
json={ json={
"projectName": "report1", "projectName": projectName,
"week": 1, "week": 1,
"developmentTime": 10, "developmentTime": 10,
"meetingTime": 5, "meetingTime": 5,
@ -89,9 +95,151 @@ def test_submit_report():
assert response.status_code == 200, "Submit report failed" assert response.status_code == 200, "Submit report failed"
print("Submit report successful") print("Submit report successful")
# Test function to get a weekly report
def test_get_weekly_report():
token = login(username, "always_same").json()["token"]
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName , "week": 1}
)
print(response.text)
assert response.status_code == 200, "Get weekly report failed"
# Tests getting a project by id
def test_get_project():
token = login(username, "always_same").json()["token"]
response = requests.get(
getProjectPath + "/1", # Assumes that the project with id 1 exists
headers={"Authorization": "Bearer " + token},
)
print(response.text)
assert response.status_code == 200, "Get project failed"
# 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"
print("Registering with username: ", admin_username, " and password: ", admin_password)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
print(response.text)
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(promoteToAdminPath, json={"username": admin_username}, headers={"Authorization": "Bearer " + admin_token})
print(response.text)
assert response.status_code == 200, "Promote to site admin failed"
print("Admin promoted to site admin successfully")
# Create a new user to add to the project
new_user = randomString()
register(new_user, "new_user_password")
# Add the new user to the project as a member
response = requests.put(
addUserToProjectPath,
json={"projectName": projectName, "username": new_user, "role": "member"},
headers={"Authorization": "Bearer " + admin_token},
)
print(response.text)
assert response.status_code == 200, "Add user to project failed"
print("Add user to project successful")
# Check if the user is added to the project
response = requests.get(
getUserProjectsPath,
json={"username": new_user},
headers={"Authorization": "Bearer " + admin_token},
)
print(response.text)
assert response.status_code == 200, "Get user projects failed"
print("got user projects successfully")
# Test function to sign a report
def test_sign_report():
# Create a project manager user
project_manager = randomString()
register(project_manager, "project_manager_password")
# Register an admin
admin_username = randomString()
admin_password = "admin_password2"
print("Registering with username: ", admin_username, " and password: ", admin_password)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
print(response.text)
# Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(promoteToAdminPath, json={"username": admin_username}, headers={"Authorization": "Bearer " + admin_token})
response = requests.put(
addUserToProjectPath,
json={"projectName": projectName, "username": project_manager, "role": "project_manager"},
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Add project manager to project failed"
print("Project manager added to project successfully")
# Log in as the project manager
project_manager_token = login(project_manager, "project_manager_password").json()["token"]
# Submit a report for the project
token = login(username, "always_same").json()["token"]
response = requests.post(
submitReportPath,
json={
"projectName": projectName,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
headers={"Authorization": "Bearer " + token},
)
assert response.status_code == 200, "Submit report failed"
print("Submit report successful")
# Retrieve the report ID
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName , "week": 1}
)
print(response.text)
report_id = response.json()["reportId"]
# Sign the report as the project manager
response = requests.post(
signReportPath,
json={"reportId": report_id},
headers={"Authorization": "Bearer " + project_manager_token},
)
assert response.status_code == 200, "Sign report failed"
print("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}
)
print(response.text)
if __name__ == "__main__": if __name__ == "__main__":
test_create_user() test_create_user()
test_login() test_login()
test_add_project() test_add_project()
test_submit_report() test_submit_report()
test_get_weekly_report()
test_get_project()
test_sign_report()
test_add_user_to_project()