Compare commits

..

2 commits

Author SHA1 Message Date
thugborean
745786b7fd Merge remote-tracking branch 'origin/dev' into borean 2024-04-03 19:02:01 +02:00
borean
60b4bf2fc2 removed mandatory return 2024-03-31 18:18:51 +02:00
72 changed files with 1454 additions and 3697 deletions

View file

@ -47,7 +47,7 @@ itest:
make build make build
./bin/$(PROC_NAME) >/dev/null 2>&1 & ./bin/$(PROC_NAME) >/dev/null 2>&1 &
sleep 1 # Adjust if needed sleep 1 # Adjust if needed
python ../testing/testing.py || pkill $(PROC_NAME) python ../testing.py
pkill $(PROC_NAME) pkill $(PROC_NAME)
# Get dependencies target # Get dependencies target

View file

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

View file

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

View file

@ -2,12 +2,11 @@ package database
import ( import (
"embed" "embed"
"encoding/json"
"errors" "errors"
"fmt"
"path/filepath" "path/filepath"
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@ -23,6 +22,8 @@ type Database interface {
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
DeleteProject(name string, username string) error DeleteProject(name string, username string) error
Migrate() error
MigrateSampleData() error
GetProjectId(projectname string) (int, error) GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
@ -40,21 +41,15 @@ type Database interface {
SignWeeklyReport(reportId int, projectManagerId int) error SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error) IsSiteAdmin(username string) (bool, error)
IsProjectManager(username string, projectname string) (bool, error) IsProjectManager(username string, projectname string) (bool, error)
ReportStatistics(username string, projectName string) (*types.Statistics, error)
GetProjectTimes(projectName string) (map[string]int, error) GetProjectTimes(projectName string) (map[string]int, error)
UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
RemoveProject(projectname string) error RemoveProject(projectname string) error
GetUserName(id int) (string, error)
UnsignWeeklyReport(reportId int, projectManagerId int) error
DeleteReport(reportID int) error
ChangeProjectName(projectName string, newProjectName string) error
ChangeUserPassword(username string, password string) error
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use // Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct { type Db struct {
*sqlx.Tx *sqlx.DB
} }
type UserProjectMember struct { type UserProjectMember struct {
@ -96,19 +91,8 @@ const removeUserFromProjectQuery = `DELETE FROM user_roles
WHERE user_id = (SELECT id FROM users WHERE username = ?) WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)` AND project_id = (SELECT id FROM projects WHERE name = ?)`
const reportStatistics = `SELECT SUM(development_time) AS total_development_time,
SUM(meeting_time) AS total_meeting_time,
SUM(admin_time) AS total_admin_time,
SUM(own_work_time) AS total_own_work_time,
SUM(study_time) AS total_study_time,
SUM(testing_time) AS total_testing_time
FROM weekly_reports
WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
GROUP BY user_id, project_id`
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) sqlx.DB { func DbConnect(dbpath string) Database {
// Open the database // Open the database
db, err := sqlx.Connect("sqlite", dbpath) db, err := sqlx.Connect("sqlite", dbpath)
if err != nil { if err != nil {
@ -121,25 +105,7 @@ func DbConnect(dbpath string) sqlx.DB {
panic(err) panic(err)
} }
return *db return &Db{db}
}
func (d *Db) ReportStatistics(username string, projectName string) (*types.Statistics, error) {
var result types.Statistics
err := d.Get(&result, reportStatistics, username, projectName)
if err != nil {
return nil, err
}
serialized, err := json.Marshal(result)
if err != nil {
return nil, err
}
log.Info(string(serialized))
return &result, nil
} }
func (d *Db) CheckUser(username string, password string) bool { func (d *Db) CheckUser(username string, password string) bool {
@ -243,15 +209,25 @@ func (d *Db) GetProjectId(projectname string) (int, error) {
// Creates a new project in the database, associated with a user // Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error { func (d *Db) AddProject(name string, description string, username string) error {
tx := d.MustBegin()
// Insert the project into the database // Insert the project into the database
_, err := d.Exec(projectInsert, name, description, username) _, err := tx.Exec(projectInsert, name, description, username)
if err != nil { if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err return err
} }
// Add creator to project as project manager // Add creator to project as project manager
_, err = d.Exec(addUserToProject, username, name, "project_manager") _, err = tx.Exec(addUserToProject, username, name, "project_manager")
if err != nil { if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err return err
} }
@ -259,7 +235,16 @@ func (d *Db) AddProject(name string, description string, username string) error
} }
func (d *Db) DeleteProject(projectID string, username string) error { func (d *Db) DeleteProject(projectID string, username string) error {
_, err := d.Exec(deleteProject, projectID, username) tx := d.MustBegin()
_, err := tx.Exec(deleteProject, projectID, username)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("error rolling back transaction: %v, delete error: %v", rollbackErr, err)
}
panic(err)
}
return err return err
} }
@ -364,14 +349,9 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err return err
} }
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager // Retrieve the project ID associated with the project manager
var managerProjectID int var managerProjectID int
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId) err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
if err != nil { if err != nil {
return err return err
} }
@ -386,36 +366,6 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err return err
} }
func (d *Db) UnsignWeeklyReport(reportId int, projectManagerId int) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
if err != nil {
return err
}
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId)
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors.New("project manager doesn't have permission to unsign the report")
}
// Update the signed_by field of the specified report
_, err = d.Exec("UPDATE weekly_reports SET signed_by = NULL WHERE report_id = ?;", reportId)
return err
}
func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) { func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) {
// Define the SQL query to fetch unsigned reports for a given user // Define the SQL query to fetch unsigned reports for a given user
query := ` query := `
@ -483,7 +433,7 @@ func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func Migrate(db sqlx.DB) 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 {
@ -495,7 +445,7 @@ func Migrate(db sqlx.DB) error {
return nil return nil
} }
tr := db.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -581,7 +531,7 @@ func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, d
} }
// MigrateSampleData applies sample data to the database. // MigrateSampleData applies sample data to the database.
func MigrateSampleData(db sqlx.DB) error { func (d *Db) MigrateSampleData() error {
// Insert sample data // Insert sample data
files, err := sampleData.ReadDir("sample_data") files, err := sampleData.ReadDir("sample_data")
if err != nil { if err != nil {
@ -591,7 +541,7 @@ func MigrateSampleData(db sqlx.DB) error {
if len(files) == 0 { if len(files) == 0 {
println("No sample data files found") println("No sample data files found")
} }
tr := db.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -628,7 +578,7 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
WHERE projects.name = ? WHERE projects.name = ?
` `
rows, err := d.Query(query, projectName) rows, err := d.DB.Query(query, projectName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -661,25 +611,3 @@ func (d *Db) RemoveProject(projectname string) error {
_, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname)
return err return err
} }
func (d *Db) GetUserName(id int) (string, error) {
var username string
err := d.Get(&username, "SELECT username FROM users WHERE id = ?", id)
return username, err
}
func (d *Db) DeleteReport(reportID int) error {
_, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID)
return err
}
// ChangeProjectName is a handler that changes the name of a project
func (d *Db) ChangeProjectName(projectName string, newProjectName string) error {
_, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName)
return err
}
func (d *Db) ChangeUserPassword(username string, password string) error {
_, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username)
return err
}

View file

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

View file

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

View file

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

View file

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

View file

@ -44,10 +44,6 @@ func PromoteToPm(c *fiber.Ctx) error {
// Add the user to the project with the specified role // Add the user to the project with the specified role
err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager") err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager")
if err != nil {
log.Info("Error promoting user to project manager:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message // Return success message
log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project) log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project)

View file

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

View file

@ -38,7 +38,7 @@ func GetAllWeeklyReports(c *fiber.Ctx) error {
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
if !(pm || target_user == username) { if pm == false && target_user != username {
log.Info("Unauthorized access") log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access") return c.Status(403).SendString("Unauthorized access")
} }

View file

@ -47,7 +47,7 @@ func GetWeeklyReport(c *fiber.Ctx) error {
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
if !(pm || target_user == username) { if pm == false && target_user != username {
log.Info("Unauthorized access") log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access") return c.Status(403).SendString("Unauthorized access")
} }

View file

@ -1,56 +0,0 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func GetStatistics(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name from query parameters
projectName := c.Query("projectName")
userNameParam := c.Query("userName")
log.Info(username, " trying to get statistics for project: ", projectName)
if projectName == "" {
log.Info("Missing project name")
return c.Status(400).SendString("Missing project name")
}
// Check if the user is a project manager
pm, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
// Bail if the user is not a PM or checking its own statistics
if !pm && userNameParam != "" && userNameParam != username {
log.Info("Unauthorized access for user: ", username, "trying to access project: ", projectName, "statistics for user: ", userNameParam)
return c.Status(403).SendString("Unauthorized access")
}
if pm && userNameParam != "" {
username = userNameParam
}
// Retrieve statistics for the project from the database
statistics, err := db.GetDb(c).ReportStatistics(username, projectName)
if err != nil {
log.Error("Error getting statistics for project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning statistics")
// Return the retrieved statistics
return c.JSON(statistics)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -59,13 +59,13 @@ func main() {
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database // Migrate the database
if err = database.Migrate(db); err != nil { if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err) fmt.Println("Error migrating database: ", err)
os.Exit(1) os.Exit(1)
} }
// Migrate sample data, should not be used in production // Migrate sample data, should not be used in production
if err = database.MigrateSampleData(db); err != nil { if err = db.MigrateSampleData(); err != nil {
fmt.Println("Error migrating sample data: ", err) fmt.Println("Error migrating sample data: ", err)
os.Exit(1) os.Exit(1)
} }
@ -103,14 +103,12 @@ func main() {
// userGroup := api.Group("/user") // Not currently in use // userGroup := api.Group("/user") // Not currently in use
api.Get("/users/all", users.ListAllUsers) api.Get("/users/all", users.ListAllUsers)
api.Get("/project/getAllUsers", users.GetAllUsersProject) api.Get("/project/getAllUsers", users.GetAllUsersProject)
api.Get("/username", users.GetUserName)
api.Post("/login", users.Login) api.Post("/login", users.Login)
api.Post("/register", users.Register) api.Post("/register", users.Register)
api.Post("/loginrenew", users.LoginRenew) api.Post("/loginrenew", users.LoginRenew)
api.Post("/promoteToAdmin", users.PromoteToAdmin) api.Post("/promoteToAdmin", users.PromoteToAdmin)
api.Put("/changeUserName", users.ChangeUserName) api.Put("/changeUserName", users.ChangeUserName)
api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches
api.Put("/changeUserPassword/:username", users.ChangeUserPassword)
// All project related routes // All project related routes
// projectGroup := api.Group("/project") // Not currently in use // projectGroup := api.Group("/project") // Not currently in use
@ -126,19 +124,15 @@ func main() {
api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject)
api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/removeProject/:projectName", projects.RemoveProject)
api.Delete("/project/:projectID", projects.DeleteProject) api.Delete("/project/:projectID", projects.DeleteProject)
api.Put("/changeProjectName/:projectName", projects.ChangeProjectName)
// All report related routes // All report related routes
// reportGroup := api.Group("/report") // Not currently in use // reportGroup := api.Group("/report") // Not currently in use
api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Get("/getWeeklyReport", reports.GetWeeklyReport)
api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports)
api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports)
api.Get("/getStatistics", reports.GetStatistics)
api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport)
api.Put("/signReport/:reportId", reports.SignReport) api.Put("/signReport/:reportId", reports.SignReport)
api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport)
api.Put("/unsignReport/:reportId", reports.UnsignReport)
api.Delete("/deleteReport/:reportId", reports.DeleteReport)
// 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))

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { AddMemberInfo } from "../Components/AddMember"; import { NewProjMember } from "../Components/AddMember";
import { ProjectRoleChange } from "../Components/ChangeRole"; import { ProjectRoleChange } from "../Components/ChangeRole";
import { projectTimes } from "../Components/GetProjectTimes"; import { projectTimes } from "../Components/GetProjectTimes";
import { ProjectMember } from "../Components/GetUsersInProject"; import { ProjectMember } from "../Components/GetUsersInProject";
@ -11,7 +11,6 @@ import {
NewProject, NewProject,
WeeklyReport, WeeklyReport,
StrNameChange, StrNameChange,
Statistics,
} from "../Types/goTypes"; } from "../Types/goTypes";
/** /**
@ -198,7 +197,7 @@ interface API {
): Promise<APIResponse<void>>; ): Promise<APIResponse<void>>;
addUserToProject( addUserToProject(
addMemberInfo: AddMemberInfo, user: NewProjMember,
token: string, token: string,
): Promise<APIResponse<void>>; ): Promise<APIResponse<void>>;
@ -222,15 +221,6 @@ interface API {
*/ */
signReport(reportId: number, token: string): Promise<APIResponse<string>>; signReport(reportId: number, token: string): Promise<APIResponse<string>>;
/**
* Unsigns a report. Keep in mind that the user which the token belongs to must be
* the project manager of the project the report belongs to.
*
* @param {number} reportId The id of the report to sign
* @param {string} token The authentication token
*/
unsignReport(reportId: number, token: string): Promise<APIResponse<string>>;
/** /**
* Promotes a user to project manager within a project. * Promotes a user to project manager within a project.
* *
@ -243,58 +233,6 @@ interface API {
projectName: string, projectName: string,
token: string, token: string,
): Promise<APIResponse<string>>; ): Promise<APIResponse<string>>;
/**
* Get the username from the id
* @param {number} id The id of the user
* @param {string} token Your token
*/
getUsername(id: number, token: string): Promise<APIResponse<string>>;
/**
* Deletes a WeeklyReport from the database
* @param {number} reportId The id of the report to delete
* @param {string} token The authentication token
*/
deleteWeeklyReport(
reportId: number,
token: string,
): Promise<APIResponse<string>>;
/**
* Retrieves the total time spent on a project for a particular user (the user is determined by the token)
*
* @param {string} projectName The name of the project
* @param {string} token The authentication token
*/
getStatistics(
projectName: string,
token: string,
userName?: string,
): Promise<APIResponse<Statistics>>;
/**
* Changes the name of a project
* @param {string} projectName The name of the project
* @param {string} newProjectName The new name of the project
* @param {string} token The authentication token
*/
changeProjectName(
projectName: string,
newProjectName: string,
token: string,
): Promise<APIResponse<string>>;
/**
* Changes the password of a user
* @param {string} username The username of the user
* @param {string} newPassword The new password
* @param {string} token The authentication token
*/
changeUserPassword(
username: string,
newPassword: string,
token: string,
): Promise<APIResponse<string>>;
} }
/** An instance of the API */ /** An instance of the API */
@ -404,20 +342,18 @@ export const api: API = {
}, },
async addUserToProject( async addUserToProject(
addMemberInfo: AddMemberInfo, user: NewProjMember,
token: string, token: string,
): Promise<APIResponse<void>> { ): Promise<APIResponse<void>> {
try { try {
const response = await fetch( const response = await fetch("/api/addUserToProject", {
`/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`,
{
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}, },
}, body: JSON.stringify(user),
); });
if (!response.ok) { if (!response.ok) {
return { success: false, message: "Failed to add member" }; return { success: false, message: "Failed to add member" };
@ -701,11 +637,7 @@ export const api: API = {
}); });
if (!response.ok) { if (!response.ok) {
return { return { success: false, message: "Failed to login" };
success: false,
data: `${response.status}`,
message: "Failed to login",
};
} else { } else {
const data = (await response.json()) as { token: string }; // Update the type of 'data' const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token }; return { success: true, data: data.token };
@ -906,29 +838,6 @@ export const api: API = {
} }
}, },
async unsignReport(
reportId: number,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/unsignReport/${reportId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to unsign report" };
} else {
return { success: true, message: "Report unsigned" };
}
} catch (e) {
return { success: false, message: "Failed to unsign report" };
}
},
async promoteToPm( async promoteToPm(
userName: string, userName: string,
projectName: string, projectName: string,
@ -959,129 +868,4 @@ export const api: API = {
} }
return { success: true, message: "User promoted to project manager" }; return { success: true, message: "User promoted to project manager" };
}, },
async getUsername(id: number, token: string): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/username?userId=${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to get username" };
} else {
const data = (await response.json()) as string;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get username" };
}
},
async deleteWeeklyReport(
reportId: number,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(`/api/deleteReport/${reportId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return { success: false, message: "Failed to delete report" };
} else {
return { success: true, message: "Report deleted" };
}
} catch (e) {
return { success: false, message: "Failed to delete report" };
}
},
async getStatistics(
projectName: string,
token: string,
userName?: string,
): Promise<APIResponse<Statistics>> {
try {
const response = await fetch(
`/api/getStatistics/?projectName=${projectName}&userName=${userName ?? ""}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to get statistics" };
} else {
const data = (await response.json()) as Statistics;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get statistics" };
}
},
async changeProjectName(
projectName: string,
newProjectName: string,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(
`/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to change project name" };
} else {
return { success: true, message: "Project name changed" };
}
} catch (e) {
return { success: false, message: "Failed to change project name" };
}
},
async changeUserPassword(
username: string,
newPassword: string,
token: string,
): Promise<APIResponse<string>> {
try {
const response = await fetch(
`/api/changeUserPassword/${username}?newPassword=${newPassword}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to change password" };
} else {
return { success: true, message: "Password changed" };
}
} catch (e) {
return { success: false, message: "Failed to change password" };
}
},
}; };

View file

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

View file

@ -1,105 +1,90 @@
import { useState } from "react"; import { useState } from "react";
import { api } from "../API/API"; import { APIResponse, api } from "../API/API";
import { NewProject } from "../Types/goTypes"; import { NewProject } from "../Types/goTypes";
import InputField from "./InputField";
import Logo from "../assets/Logo.svg"; import Logo from "../assets/Logo.svg";
import Button from "./Button"; import Button from "./Button";
import { useNavigate } from "react-router-dom";
import ProjectNameInput from "./Inputs/ProjectNameInput"; /**
import DescriptionInput from "./Inputs/DescriptionInput"; * Tries to add a project to the system
import { alphanumeric } from "../Data/regex"; * @param {Object} props - Project name and description
import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; * @returns {boolean} True if created, false if not
*/
function CreateProject(props: { name: string; description: string }): void {
const project: NewProject = {
name: props.name,
description: props.description,
};
api
.createProject(project, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {
if (response.success) {
alert("Project added!");
} else {
alert("Project NOT added!");
console.error(response.message);
}
})
.catch((error) => {
alert("Project NOT added!");
console.error("An error occurred during creation:", error);
});
}
/** /**
* Provides UI for adding a project to the system. * Provides UI for adding a project to the system.
* @returns {JSX.Element} - Returns the component UI for adding a project * @returns {JSX.Element} - Returns the component UI for adding a project
*/ */
function AddProject(): JSX.Element { function AddProject() {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const navigate = useNavigate();
/**
* Tries to add a project to the system
*/
const handleCreateProject = async (): Promise<void> => {
if (
!alphanumeric.test(name) ||
name.length > projNameHighLimit ||
name.length < projNameLowLimit
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (description.length > projNameHighLimit) {
alert("Please provide valid description: \n-Max 100 characters");
return;
}
const project: NewProject = {
name: name.replace(/ /g, ""),
description: description.trim(),
};
try {
const response = await api.createProject(
project,
localStorage.getItem("accessToken") ?? "",
);
if (response.success) {
alert(`${project.name} added!`);
setDescription("");
setName("");
navigate("/admin");
} else {
alert("Project not added, name could be taken");
console.error(response.message);
}
} catch (error) {
alert("Project not added");
console.error(error);
}
};
return ( return (
<div className="flex flex-col h-fit w-screen items-center justify-center"> <div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form <form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit" id="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) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void handleCreateProject(); CreateProject({
name: name,
description: description,
});
}} }}
> >
<img <img
src={Logo} src={Logo}
className="logo w-[7vw] self-center mb-10 mt-10" className="logo w-[7vw] mb-10 mt-10"
alt="TTIME Logo" alt="TTIME Logo"
/> />
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> <h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project Create a new project
</h3> </h3>
<ProjectNameInput <div className="space-y-3">
name={name} <InputField
label="Name"
type="text"
value={name}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setName(e.target.value); setName(e.target.value);
}} }}
/> />
<div className="p-2"></div> <InputField
<DescriptionInput label="Description"
desc={description} type="text"
value={description}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setDescription(e.target.value); setDescription(e.target.value);
}} }}
placeholder={"Description (Optional)"}
/> />
<div className="flex self-center mt-4 justify-between"> </div>
<div className="flex items-center justify-between">
<Button <Button
text="Create" text="Create"
onClick={(): void => {
return;
}}
type="submit" type="submit"
/> />
</div> </div>

View file

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

View file

@ -17,7 +17,7 @@ function AllTimeReportsInProject(): JSX.Element {
useEffect(() => { useEffect(() => {
const getWeeklyReports = async (): Promise<void> => { const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllWeeklyReportsForUser( const response = await api.getWeeklyReportsForUser(
projectName ?? "", projectName ?? "",
token, token,
); );
@ -37,9 +37,9 @@ function AllTimeReportsInProject(): JSX.Element {
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]">
{weeklyReports.map((newWeeklyReport, index) => ( {weeklyReports.map((newWeeklyReport, index) => (
<Link <Link
to={`/editTimeReport/${projectName}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`} to={`/editTimeReport/${projectName}/${newWeeklyReport.week}`}
key={index} key={index}
className="border-b-2 border-black w-full cursor-pointer hover:font-extrabold" className="border-b-2 border-black w-full"
> >
<div className="flex justify-between"> <div className="flex justify-between">
<h1> <h1>

View file

@ -17,10 +17,10 @@ function AllTimeReportsInProject(): JSX.Element {
useEffect(() => { useEffect(() => {
const getWeeklyReports = async (): Promise<void> => { const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllWeeklyReportsForUser( const response = await api.getWeeklyReportsForDifferentUser(
projectName ?? "", projectName ?? "",
token,
username ?? "", username ?? "",
token,
); );
console.log(response); console.log(response);
if (response.success) { if (response.success) {
@ -31,7 +31,7 @@ function AllTimeReportsInProject(): JSX.Element {
}; };
void getWeeklyReports(); void getWeeklyReports();
}, [projectName, username]); }, []);
return ( return (
<> <>
@ -39,9 +39,9 @@ function AllTimeReportsInProject(): JSX.Element {
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]">
{weeklyReports.map((newWeeklyReport, index) => ( {weeklyReports.map((newWeeklyReport, index) => (
<Link <Link
to={`/editOthersTR/${projectName}/${username}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`} to={`/editOthersTR/${projectName}/${username}/${newWeeklyReport.week}`}
key={index} key={index}
className="border-b-2 border-black w-full hover:font-extrabold" className="border-b-2 border-black w-full"
> >
<div className="flex justify-between"> <div className="flex justify-between">
<h1> <h1>
@ -60,7 +60,7 @@ function AllTimeReportsInProject(): JSX.Element {
</h1> </h1>
<h1> <h1>
<span className="font-bold">{"Signed: "}</span> <span className="font-bold">{"Signed: "}</span>
{newWeeklyReport.signedBy ? "YES" : "NO"} NO
</h1> </h1>
</div> </div>
</Link> </Link>

View file

@ -13,7 +13,7 @@ function Button({
type, type,
}: { }: {
text: string; text: string;
onClick: () => void; onClick?: () => void;
type: "submit" | "button" | "reset"; type: "submit" | "button" | "reset";
}): JSX.Element { }): JSX.Element {
return ( return (

View file

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

View file

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

View file

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

View file

@ -2,15 +2,8 @@ import { APIResponse, api } from "../API/API";
import { StrNameChange } from "../Types/goTypes"; import { StrNameChange } from "../Types/goTypes";
function ChangeUsername(props: { nameChange: StrNameChange }): void { function ChangeUsername(props: { nameChange: StrNameChange }): void {
if ( if (props.nameChange.newName === "") {
props.nameChange.newName === "" || alert("You have to select a new name");
props.nameChange.newName === props.nameChange.prevName
) {
alert("You have to give a new name\n\nName not changed");
return;
}
if (props.nameChange.prevName === localStorage.getItem("username")) {
alert("You cannot change admin name");
return; return;
} }
api api
@ -20,7 +13,7 @@ function ChangeUsername(props: { nameChange: StrNameChange }): void {
alert("Name changed successfully"); alert("Name changed successfully");
location.reload(); location.reload();
} else { } else {
alert("Name not changed, name could be taken"); alert("Name not changed");
console.error(response.message); console.error(response.message);
} }
}) })

View file

@ -3,14 +3,17 @@ import { Link, useParams } from "react-router-dom";
import { api } from "../API/API"; import { api } from "../API/API";
import { WeeklyReport } from "../Types/goTypes"; import { WeeklyReport } from "../Types/goTypes";
/**
* Renders a component that displays the projects a user is a part of and links to the projects start-page.
* @returns The JSX element representing the component.
*/
function DisplayUserProject(): JSX.Element { function DisplayUserProject(): JSX.Element {
const { projectName } = useParams(); const { projectName } = useParams();
const [unsignedReports, setUnsignedReports] = useState<WeeklyReport[]>([]); const [unsignedReports, setUnsignedReports] = useState<WeeklyReport[]>([]);
const [usernames, setUsernames] = useState<string[]>([]); //const navigate = useNavigate();
const token = localStorage.getItem("accessToken") ?? "";
useEffect(() => { useEffect(() => {
const getUnsignedReports = async (): Promise<void> => { const getUnsignedReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getUnsignedReportsInProject( const response = await api.getUnsignedReportsInProject(
projectName ?? "", projectName ?? "",
token, token,
@ -18,21 +21,13 @@ function DisplayUserProject(): JSX.Element {
console.log(response); console.log(response);
if (response.success) { if (response.success) {
setUnsignedReports(response.data ?? []); setUnsignedReports(response.data ?? []);
const usernamesPromises = (response.data ?? []).map((report) =>
api.getUsername(report.userId, token),
);
const usernamesResponses = await Promise.all(usernamesPromises);
const usernames = usernamesResponses.map(
(res) => (res.data as { username?: string }).username ?? "",
);
setUsernames(usernames);
} else { } else {
console.error(response.message); console.error(response.message);
} }
}; };
void getUnsignedReports(); void getUnsignedReports();
}, [projectName, token]); }, [projectName]); // Include 'projectName' in the dependency array
return ( return (
<> <>
@ -44,8 +39,8 @@ function DisplayUserProject(): JSX.Element {
<h1 key={index} className="border-b-2 border-black w-full"> <h1 key={index} className="border-b-2 border-black w-full">
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex"> <div className="flex">
<span className="ml-6 mr-2 font-bold">Username:</span> <span className="ml-6 mr-2 font-bold">UserID:</span>
<h1>{usernames[index]}</h1>{" "} <h1>{unsignedReport.userId}</h1>
<span className="ml-6 mr-2 font-bold">Week:</span> <span className="ml-6 mr-2 font-bold">Week:</span>
<h1>{unsignedReport.week}</h1> <h1>{unsignedReport.week}</h1>
<span className="ml-6 mr-2 font-bold">Total Time:</span> <span className="ml-6 mr-2 font-bold">Total Time:</span>
@ -63,9 +58,9 @@ function DisplayUserProject(): JSX.Element {
<div className="flex"> <div className="flex">
<div className="ml-auto flex space-x-4"> <div className="ml-auto flex space-x-4">
<Link <Link
to={`/PMViewUnsignedReport/${projectName}/${usernames[index]}/${unsignedReport.week}`} to={`/PMViewUnsignedReport/${projectName}/${unsignedReport.userId}/${unsignedReport.week}`}
> >
<h1 className="cursor-pointer font-bold hover:font-extrabold hover:underline"> <h1 className="underline cursor-pointer font-bold">
View Report View Report
</h1> </h1>
</Link> </Link>

View file

@ -45,7 +45,7 @@ function DisplayUserProject(): JSX.Element {
onClick={() => void handleProjectClick(project.name)} onClick={() => void handleProjectClick(project.name)}
key={project.id} key={project.id}
> >
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
{project.name} {project.name}
</h1> </h1>
</div> </div>

View file

@ -18,13 +18,12 @@ export default function GetWeeklyReport(): JSX.Element {
const [testingTime, setTestingTime] = useState(0); const [testingTime, setTestingTime] = useState(0);
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const { projectName, fetchedWeek, signedOrUnsigned } = useParams<{ const { projectName, fetchedWeek } = useParams<{
projectName: string; projectName: string;
fetchedWeek: string; fetchedWeek: string;
signedOrUnsigned: string;
}>(); }>();
const username = localStorage.getItem("userName") ?? ""; const username = localStorage.getItem("userName") ?? "";
console.log(projectName, fetchedWeek, signedOrUnsigned); console.log(projectName, fetchedWeek);
useEffect(() => { useEffect(() => {
const fetchWeeklyReport = async (): Promise<void> => { const fetchWeeklyReport = async (): Promise<void> => {
@ -60,7 +59,7 @@ export default function GetWeeklyReport(): JSX.Element {
}; };
void fetchWeeklyReport(); void fetchWeeklyReport();
}, [projectName, fetchedWeek, signedOrUnsigned, token]); }, [projectName, fetchedWeek, token]);
const handleUpdateWeeklyReport = async (): Promise<void> => { const handleUpdateWeeklyReport = async (): Promise<void> => {
const updateWeeklyReport: UpdateWeeklyReport = { const updateWeeklyReport: UpdateWeeklyReport = {
@ -140,12 +139,6 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -175,12 +168,6 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -210,12 +197,6 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -245,12 +226,6 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -280,12 +255,6 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -315,18 +284,11 @@ export default function GetWeeklyReport(): JSX.Element {
) )
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{signedOrUnsigned !== "signed" && (
<Button <Button
text="Submit changes" text="Submit changes"
onClick={(): void => { onClick={(): void => {
@ -334,7 +296,6 @@ export default function GetWeeklyReport(): JSX.Element {
}} }}
type="submit" type="submit"
/> />
)}
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,6 +1,6 @@
//info: Header component to display the header of the page including the logo and user information where thr user can logout //info: Header component to display the header of the page including the logo and user information where thr user can logout
import { useState } from "react"; import { useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link } from "react-router-dom";
import backgroundImage from "../assets/1.jpg"; import backgroundImage from "../assets/1.jpg";
/** /**
@ -9,33 +9,23 @@ import backgroundImage from "../assets/1.jpg";
*/ */
function Header(): JSX.Element { function Header(): JSX.Element {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const username = localStorage.getItem("username");
const navigate = useNavigate();
const handleLogout = (): void => { const handleLogout = (): void => {
localStorage.clear(); localStorage.clear();
}; };
const handleNavigation = (): void => {
if (username === "admin") {
navigate("/admin");
} else {
navigate("/yourProjects");
}
};
return ( return (
<header <header
className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover" className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover"
style={{ backgroundImage: `url(${backgroundImage})` }} style={{ backgroundImage: `url(${backgroundImage})` }}
> >
<div onClick={handleNavigation}> <Link to="/your-projects">
<img <img
src="/src/assets/Logo.svg" src="/src/assets/Logo.svg"
alt="TTIME Logo" alt="TTIME Logo"
className="w-11 h-14 cursor-pointer" className="w-11 h-14 cursor-pointer"
/> />
</div> </Link>
<div <div
className="relative" className="relative"

View file

@ -4,21 +4,19 @@
* @returns {JSX.Element} The input field * @returns {JSX.Element} The input field
* @example * @example
* <InputField * <InputField
* label="Example"
* placeholder="New placeholder"
* type="text" * type="text"
* value={example} * label="Example"
* onChange={(e) => { * onChange={(e) => {
* setExample(e.target.value); * setExample(e.target.value);
* }} * }}
* value={example}
* /> * />
*/ */
function InputField(props: { function InputField(props: {
label?: string; label: string;
placeholder?: string; type: string;
type?: string; value: string;
value?: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className=""> <div className="">
@ -32,7 +30,7 @@ function InputField(props: {
className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id={props.label} id={props.label}
type={props.type} type={props.type}
placeholder={props.placeholder} placeholder={props.label}
value={props.value} value={props.value}
onChange={props.onChange} onChange={props.onChange}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -62,7 +62,7 @@ export default function NewWeeklyReport(): JSX.Element {
const success = await handleNewWeeklyReport(); const success = await handleNewWeeklyReport();
if (!success) { if (!success) {
alert( alert(
"Error occurred! Your connection to the server might be lost or a time report for this week already exists, please check your connection or go to the edit page to edit your report or change week number.", "A Time Report for this week already exists, please go to the edit page to edit it or change week number.",
); );
return; return;
} }

View file

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

View file

@ -8,22 +8,22 @@ function PMProjectMenu(): JSX.Element {
<h1 className="font-bold text-[30px] mb-[20px]">{projectName}</h1> <h1 className="font-bold text-[30px] mb-[20px]">{projectName}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[5vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[5vh] p-[30px]">
<Link to={`/timeReports/${projectName}/`}> <Link to={`/timeReports/${projectName}/`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports Your Time Reports
</h1> </h1>
</Link> </Link>
<Link to={`/newTimeReport/${projectName}`}> <Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report New Time Report
</h1> </h1>
</Link> </Link>
<Link to={`/projectMembers/${projectName}`}> <Link to={`/projectMembers/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Statistics Statistics
</h1> </h1>
</Link> </Link>
<Link to={`/unsignedReports/${projectName}`}> <Link to={`/unsignedReports/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Unsigned Time Reports Unsigned Time Reports
</h1> </h1>
</Link> </Link>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,13 +8,12 @@ import { projectTimes } from "./GetProjectTimes";
* @returns JSX.Element * @returns JSX.Element
*/ */
export default function TimePerRole(): JSX.Element { export default function TimePerRole(): JSX.Element {
const [development, setDevelopment] = useState<number>(0); const [development, setDevelopment] = useState<number>();
const [meeting, setMeeting] = useState<number>(0); const [meeting, setMeeting] = useState<number>();
const [admin, setAdmin] = useState<number>(0); const [admin, setAdmin] = useState<number>();
const [own_work, setOwnWork] = useState<number>(0); const [own_work, setOwnWork] = useState<number>();
const [study, setStudy] = useState<number>(0); const [study, setStudy] = useState<number>();
const [testing, setTesting] = useState<number>(0); const [testing, setTesting] = useState<number>();
const total = development + meeting + admin + own_work + study + testing;
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams(); const { projectName } = useParams();
@ -50,7 +49,7 @@ export default function TimePerRole(): JSX.Element {
return ( return (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]"> <h1 className="font-bold text-[30px] mb-[20px]">
Total Time Per Activity For All Members In: {projectName}{" "} Total Time Per Activity In: {projectName}{" "}
</h1> </h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center"> <div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -130,12 +129,6 @@ export default function TimePerRole(): JSX.Element {
/> />
</td> </td>
</tr> </tr>
<tr className="h-[10vh] font-bold font-">
<td>In Total:</td>
<td>
<h1>{total}</h1>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View file

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

View file

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

View file

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

View file

@ -16,12 +16,12 @@ function UserProjectMenu(): JSX.Element {
<h1 className="font-bold text-[30px] mb-[20px]">{projectName}</h1> <h1 className="font-bold text-[30px] mb-[20px]">{projectName}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<Link to={`/timeReports/${projectName}/`}> <Link to={`/timeReports/${projectName}/`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports Your Time Reports
</h1> </h1>
</Link> </Link>
<Link to={`/newTimeReport/${projectName}`}> <Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report New Time Report
</h1> </h1>
</Link> </Link>

View file

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

View file

@ -31,9 +31,8 @@ export default function GetOtherUsersReport(): JSX.Element {
projectName ?? "", projectName ?? "",
fetchedWeek?.toString() ?? "0", fetchedWeek?.toString() ?? "0",
token, token,
username ?? "",
); );
console.log(response);
if (response.success) { if (response.success) {
const report: WeeklyReport = response.data ?? { const report: WeeklyReport = response.data ?? {
reportId: 0, reportId: 0,
@ -63,33 +62,25 @@ export default function GetOtherUsersReport(): JSX.Element {
void fetchUsersWeeklyReport(); void fetchUsersWeeklyReport();
}); });
const handleSignWeeklyReport = async (): Promise<boolean> => { const handleSignWeeklyReport = async (): Promise<void> => {
const response = await api.signReport(reportId, token); await api.signReport(reportId, token);
if (response.success) {
return true;
} else {
return false;
}
}; };
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<> <>
<h1 className="text-[30px] font-bold"> {username}&apos;s Report</h1> <h1 className="text-[30px] font-bold">
{" "}
UserId: {username}&apos;s Report
</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center"> <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 <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void (async (): Promise<void> => { void handleSignWeeklyReport();
const success = await handleSignWeeklyReport();
if (!success) {
alert("Failed to sign report!");
return;
}
alert("Report successfully signed!"); alert("Report successfully signed!");
navigate(-1); navigate(-1);
})();
}} }}
> >
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import { ProjectListAdmin } from "../../Components/ProjectListAdmin"; import { ProjectListAdmin } from "../../Components/ProjectListAdmin";
import { Project } from "../../Types/goTypes"; import { Project } from "../../Types/goTypes";
import GetProjects from "../../Components/GetProjects"; import GetProjects from "../../Components/GetProjects";
import { useState } from "react"; import { useState } from "react";
import NavButton from "../../Components/NavButton";
function AdminManageProjects(): JSX.Element { function AdminManageProjects(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
@ -13,7 +13,14 @@ function AdminManageProjects(): JSX.Element {
setProjectsProp: setProjects, setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "", username: localStorage.getItem("username") ?? "",
}); });
const content = <ProjectListAdmin projects={projects} />; const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<ProjectListAdmin projects={projects} />
</div>
</>
);
const buttons = ( const buttons = (
<> <>
@ -26,7 +33,7 @@ function AdminManageProjects(): JSX.Element {
type="button" type="button"
/> />
</Link> </Link>
<NavButton navTo="/admin" label={"Back"} /> <BackButton />
</> </>
); );

View file

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

View file

@ -5,14 +5,14 @@ function AdminMenuPage(): JSX.Element {
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Administrator Menu</h1> <h1 className="font-bold text-[30px] mb-[20px]">Administrator Menu</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-auto 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="/adminManageUser"> <Link to="/adminManageUser">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Manage Users Manage Users
</h1> </h1>
</Link> </Link>
<Link to="/adminManageProject"> <Link to="/adminManageProject">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Manage Projects Manage Projects
</h1> </h1>
</Link> </Link>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,6 @@ 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";
import UserViewTimeReportsPage from "./Pages/UserPages/UserViewTimeReportsPage.tsx"; import UserViewTimeReportsPage from "./Pages/UserPages/UserViewTimeReportsPage.tsx";
import UserViewStatistics from "./Pages/UserPages/UserViewStatistics.tsx";
import PMChangeRole from "./Pages/ProjectManagerPages/PMChangeRole.tsx"; import PMChangeRole from "./Pages/ProjectManagerPages/PMChangeRole.tsx";
import PMOtherUsersTR from "./Pages/ProjectManagerPages/PMOtherUsersTR.tsx"; import PMOtherUsersTR from "./Pages/ProjectManagerPages/PMOtherUsersTR.tsx";
import PMProjectMembers from "./Pages/ProjectManagerPages/PMProjectMembers.tsx"; import PMProjectMembers from "./Pages/ProjectManagerPages/PMProjectMembers.tsx";
@ -23,6 +22,7 @@ import AdminManageProjects from "./Pages/AdminPages/AdminManageProjects.tsx";
import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx"; import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx";
import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx"; import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx";
import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx"; import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx";
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import NotFoundPage from "./Pages/NotFoundPage.tsx"; import NotFoundPage from "./Pages/NotFoundPage.tsx";
import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx"; import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx";
import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx"; import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx";
@ -55,13 +55,9 @@ const router = createBrowserRouter([
element: <UserViewTimeReportsPage />, element: <UserViewTimeReportsPage />,
}, },
{ {
path: "/editTimeReport/:projectName/:fetchedWeek/:signedOrUnsigned", path: "/editTimeReport/:projectName/:fetchedWeek",
element: <UserEditTimeReportPage />, element: <UserEditTimeReportPage />,
}, },
{
path: "/viewStatistics/:projectName/:username",
element: <UserViewStatistics />,
},
{ {
path: "/changeRole/:projectName/:username", path: "/changeRole/:projectName/:username",
element: <PMChangeRole />, element: <PMChangeRole />,
@ -71,7 +67,7 @@ const router = createBrowserRouter([
element: <PMOtherUsersTR />, element: <PMOtherUsersTR />,
}, },
{ {
path: "/editOthersTR/:projectName/:username/:fetchedWeek/:signedOrUnsigned", path: "/editOthersTR/:projectName/:username/:fetchedWeek",
element: <PMViewOtherUsersTR />, element: <PMViewOtherUsersTR />,
}, },
{ {
@ -102,6 +98,10 @@ const router = createBrowserRouter([
path: "/adminProjectAddMember", path: "/adminProjectAddMember",
element: <AdminProjectAddMember />, element: <AdminProjectAddMember />,
}, },
{
path: "/adminProjectStatistics",
element: <AdminProjectStatistics />,
},
{ {
path: "/addProject", path: "/addProject",
element: <AdminAddProject />, element: <AdminAddProject />,

View file

@ -34,13 +34,8 @@ getChangeUserNamePath = base_url + "/api/changeUserName"
getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport" getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport"
removeProjectPath = base_url + "/api/removeProject" removeProjectPath = base_url + "/api/removeProject"
promoteToPmPath = base_url + "/api/promoteToPm" promoteToPmPath = base_url + "/api/promoteToPm"
unsignReportPath = base_url + "/api/unsignReport"
deleteReportPath = base_url + "/api/deleteReport"
getStatisticsPath = base_url + "/api/getStatistics"
changeProjectNamePath = base_url + "/api/changeProjectName"
changeUserPasswordPath = base_url + "/api/changeUserPassword"
debug_output = False debug_output = True
def gprint(*args, **kwargs): def gprint(*args, **kwargs):
@ -154,38 +149,3 @@ def signReport(project_manager_token: string, report_id: int):
signReportPath + "/" + str(report_id), signReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + project_manager_token}, headers={"Authorization": "Bearer " + project_manager_token},
) )
def unsignReport(project_manager_token: string, report_id: int):
return requests.put(
unsignReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + project_manager_token},
)
def deleteReport(report_id: int):
return requests.delete(
deleteReportPath + "/" + str(report_id),
)
def getStatistics(token: string, projectName: string):
response = requests.get(
getStatisticsPath,
headers = {"Authorization": "Bearer " + token},
params={"projectName": projectName}
)
return response.json()
def changeProjectName(token: string, projectName: string, newProjectName: string):
response = requests.put(
changeProjectNamePath + "/" + projectName,
headers = {"Authorization": "Bearer " + token},
params={"newProjectName": newProjectName}
)
return response
def changeUserPassword(token: string, username: string, newPassword: string):
response = requests.put(
changeUserPasswordPath + "/" + username,
headers = {"Authorization": "Bearer " + token},
params={"newPassword": newPassword}
)
return response

View file

@ -215,72 +215,6 @@ def test_sign_report():
assert report_id != None, "Get report failed" assert report_id != None, "Get report failed"
gprint("test_sign_report successful") gprint("test_sign_report successful")
# Test function to unsign a report
def test_unsign_report():
# Pm user
pm_username = "pm" + randomString()
pm_password = "admin_password2"
# User to add
member_user = "member" + randomString()
member_passwd = "password"
# Name of the project to be created
project_name = "project" + randomString()
# Register and get the tokens for both users
pm_token = register_and_login(pm_username, pm_password)
member_token = register_and_login(member_user, member_passwd)
# Create the project
response = create_project(pm_token, project_name)
assert response.status_code == 200, "Create project failed"
# Add the user to the project
response = addToProject(pm_token, member_user, project_name)
# Submit a report for the project
response = submitReport(
member_token,
{
"projectName": project_name,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
)
assert response.status_code == 200, "Submit report failed"
# Retrieve the report ID
report_id = getReport(member_token, member_user, project_name)["reportId"]
# Sign the report as the project manager
response = signReport(pm_token, report_id)
assert response.status_code == 200, "Sign report failed"
dprint("Sign report successful")
# Retrieve the report ID again for confirmation
report = getReport(member_token, member_user, project_name)
dprint(report)
report_id = report["reportId"]
assert report_id != None, "Get report failed"
# Unsign the report as the project manager
response = unsignReport(pm_token, report_id)
assert response.status_code == 200, "Unsign report failed"
dprint("Unsign report successful")
# Retrieve the report ID again for confirmation
report = getReport(member_token, member_user, project_name)
assert report_id != None, "Get report failed"
dprint(report)
assert report["signedBy"] == None, "Report was not unsigned"
gprint("test_unsign_report successful")
# Test function to get weekly reports for a user in a project # Test function to get weekly reports for a user in a project
def test_get_all_weekly_reports(): def test_get_all_weekly_reports():
@ -561,189 +495,8 @@ def test_promote_to_manager():
response = promoteToManager(pm_token, member_user, project_name) response = promoteToManager(pm_token, member_user, project_name)
assert response.status_code == 200, "Promote to manager failed" assert response.status_code == 200, "Promote to manager failed"
def test_delete_report():
# Create admin
admin_username = randomString()
admin_password = "admin_password2"
dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
dprint(response.text)
# Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
# Create a new project
new_project = randomString()
response = requests.post(
addProjectPath,
json={"name": new_project, "description": "This is a project"},
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Add project failed"
# Create a new report
new_report = {
"projectName": new_project,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
}
response = submitReport(admin_token, new_report);
assert response.status_code == 200, "Submit report failed"
# Get the report ID
report_id = getReport(admin_token, admin_username, new_project)["reportId"]
assert report_id != None, "Get report failed"
# Delete the report
response = requests.delete(
deleteReportPath + "/" + str(report_id),
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Delete report failed"
# Check if the report was deleted
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + admin_token},
params={"username": admin_username, "projectName": new_project, "week": 1},
)
assert response.status_code == 500, "Report was not deleted"
gprint("test_delete_report successful")
def test_get_statistics():
# Create admin
admin_username = randomString()
admin_password = randomString()
project_name = "project" + randomString()
token = register_and_login(admin_username, admin_password)
response = create_project(token, project_name)
assert response.status_code == 200, "Create project failed"
response = submitReport(token, {
"projectName": project_name,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
})
response = submitReport(token, {
"projectName": project_name,
"week": 2,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
})
assert response.status_code == 200, "Submit report failed"
stats = getStatistics(token, project_name)
assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct"
gprint("test_get_statistics successful")
def test_project_name_change():
# Create admin
admin_username = randomString()
admin_password = randomString()
project_name = "project" + randomString()
token = register_and_login(admin_username, admin_password)
# Promote to admin
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + token},
)
response = create_project(token, project_name)
assert response.status_code == 200, "Create project failed"
response = requests.get(
getUserProjectsPath + "/" + admin_username,
headers={"Authorization": "Bearer " + token},
)
dprint(response.json())
new_project_name = "new project name " + randomString()
dprint("Changing project name from ", project_name, " to ", new_project_name)
response = changeProjectName(token, project_name, new_project_name)
response = requests.get(
getUserProjectsPath + "/" + admin_username,
headers={"Authorization": "Bearer " + token},
)
dprint(response.json())
if (response.json()[0]["name"] != new_project_name):
assert False, "Project name change failed"
assert response.status_code == 200, "Project name change failed"
gprint("test_projectNameChange successful")
def test_change_user_password():
# Create admin
admin_username = randomString()
admin_password = randomString()
user = randomString()
password = randomString()
token = register_and_login(admin_username, admin_password)
# Promote to admin
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + token},
)
_ = register_and_login(user, password)
response = changeUserPassword(token, user, "new_password")
assert response.status_code == 200, "Change user password failed"
response = login(user, "new_password")
assert response.status_code == 200, "Login failed with new password"
gprint("test_change_user_password successful")
if __name__ == "__main__": if __name__ == "__main__":
test_change_user_password()
test_project_name_change();
test_delete_report()
test_unsign_report()
test_promote_to_manager() test_promote_to_manager()
test_remove_project() test_remove_project()
test_get_user_projects() test_get_user_projects()
@ -764,4 +517,3 @@ if __name__ == "__main__":
test_change_user_name() test_change_user_name()
test_update_weekly_report() test_update_weekly_report()
test_get_other_users_report_as_pm() test_get_other_users_report_as_pm()
test_get_statistics()