Compare commits

..

87 commits

Author SHA1 Message Date
al8763be
1a412dc8a0 Merge branch 'frontend' of https://github.com/imbus64/TTime into frontend 2024-03-17 19:08:17 +01:00
Imbus
eae252c2a4 Merge branch 'frontend' of github.com:imbus64/TTime into frontend 2024-03-17 19:07:55 +01:00
Imbus
30cf0b6065 Making linter bro happy 2024-03-17 19:07:49 +01:00
al8763be
693b5fb9ae Merge branch 'dev' into frontend 2024-03-17 19:06:14 +01:00
al8763be
14668ea675 Test file for API 2024-03-17 19:05:39 +01:00
dDogge
3e00a532cf Added handler for GetWeeklyReport 2024-03-17 18:05:54 +01:00
dDogge
a77e57e496 Added GetWeeklyReport function and corresponding test 2024-03-17 17:58:02 +01:00
dDogge
c90d495636 Added new types and changed SQL since apperently sqlite does use autoincrement 2024-03-17 17:47:31 +01:00
Imbus
ec46d29423 Merge branch 'imbs' into dev 2024-03-17 16:56:00 +01:00
Imbus
d6fc0594a9 Splitting backend endpoints into smaller bits 2024-03-17 16:55:40 +01:00
Imbus
1be3ee89c2 Merge branch 'dev' of github.com:imbus64/TTime into dev 2024-03-17 16:42:02 +01:00
Imbus
ec36054bd9 Merge branch 'imbs' into dev 2024-03-17 16:41:51 +01:00
Imbus
670ed46d51 WeeklyReport type as represented in the db 2024-03-17 16:41:29 +01:00
al8763be
d57fe55074 Changes to SubmitWeeklyReport 2024-03-17 16:11:03 +01:00
al8763be
386f7c3a94 API Login return { success: true, data: data.token } 2024-03-17 15:49:34 +01:00
al8763be
d11e5e64f6 API Login return { success: true, data } data as JSON with token 2024-03-17 15:41:23 +01:00
al8763be
a28bab459a API login returns { success: true, data: token } 2024-03-17 15:36:39 +01:00
dDogge
21cc7ff8a3 Added signed_by attribute to weekly_report SQL table 2024-03-17 15:34:48 +01:00
al8763be
9511d509ca API imports fixed 2024-03-17 15:31:15 +01:00
al8763be
f5787fbc2e correct routes in main 2024-03-17 15:23:21 +01:00
al8763be
179a5196e9 Merge branch 'BumBranch' into frontend 2024-03-17 15:10:19 +01:00
al8763be
5327ac7ad1 lint + login function for API 2024-03-17 15:03:34 +01:00
Imbus
ff00b984fe Formatting and typing to make linter happy v2 2024-03-17 14:52:13 +01:00
Imbus
cab7c3bdfd Formatting and typing to make linter happy 2024-03-17 14:44:25 +01:00
al8763be
2eecf17d42 Login feature API 2024-03-17 14:43:09 +01:00
Imbus
58b74e3f04 Merge branch 'dev' into frontend 2024-03-17 14:38:44 +01:00
Imbus
7c21677310 Migrations fix in go 2024-03-17 14:38:20 +01:00
Peter KW
9bab627255 Change typ on button to submit 2024-03-17 12:57:18 +01:00
al8763be
8d0da111eb API - SubmitWeeklyReport & getUserProjects added 2024-03-17 12:35:53 +01:00
Peter KW
b6535c883d Small fix 2024-03-17 11:34:39 +01:00
Peter KW
3e73b11698 Merge branch 'frontend' into gruppPP 2024-03-17 11:02:44 +01:00
al8763be
d6d4eb3336 Merge branch 'master' into gruppDM 2024-03-17 10:51:23 +01:00
Peter KW
5e0b96ed87 Merge branch 'frontend' into gruppPP 2024-03-17 10:43:49 +01:00
Peter KW
8cf873a98b Changed so that most functionality was moved to separate components 2024-03-17 10:39:57 +01:00
Peter KW
15ec2108d3 Everything run from this file 2024-03-17 10:38:25 +01:00
Peter KW
02332c284b Login field component 2024-03-17 10:36:29 +01:00
Peter KW
94f5d3f85b Component for checking login, to be replaced with proper fetching 2024-03-17 10:35:48 +01:00
Peter KW
217b7b4a22 Input field component 2024-03-17 10:34:47 +01:00
Peter KW
2921458d82 Made backround animation on login page to its own component 2024-03-17 10:34:15 +01:00
Peter KW
c12e19770e Restructuring, less paths 2024-03-17 10:33:37 +01:00
Imbus
7f5270f536 Merge branch 'gruppDM' of github.com:imbus64/TTime into gruppDM 2024-03-16 18:38:50 +01:00
Davenludd
d58720cdbd Refactor Register component to use InputField component 2024-03-16 18:38:42 +01:00
Mattias
92a4808cdd Created new type for timereport 2024-03-16 18:38:42 +01:00
Mattias
f3c5ce57eb Submit-button has been moved to the timereport 2024-03-16 18:38:42 +01:00
Mattias
3419898c66 Created groundwork for the timereport 2024-03-16 18:38:42 +01:00
Mattias
aae05acedb Added the register component to content 2024-03-16 18:38:42 +01:00
Mattias
a225020f6f Visual fixes 2024-03-16 18:38:42 +01:00
Mattias
2ce480707a Removed duplicate path 2024-03-16 18:38:42 +01:00
pavel Hamawand
17edf6b911 backButton 2024-03-16 18:01:14 +01:00
pavel Hamawand
31e3b99448 backbutton 2024-03-16 18:00:10 +01:00
pavel Hamawand
38378c7a10 babkbutton 2024-03-16 17:59:25 +01:00
pavel Hamawand
92336ff4fd backbutton 2024-03-16 17:58:27 +01:00
pavel Hamawand
17e9fa9d0a backbutton 2024-03-16 17:57:51 +01:00
pavel Hamawand
bdd6f68470 Time / role link 2024-03-16 17:56:59 +01:00
pavel Hamawand
9aa5087871 time / activity link 2024-03-16 17:54:34 +01:00
pavel Hamawand
8164ba9040 fix back button 2024-03-16 17:50:15 +01:00
pavel Hamawand
49305d507f link to projectMembers 2024-03-16 17:48:23 +01:00
pavel Hamawand
e837f6e746 link to your time Reports 2024-03-16 17:37:09 +01:00
pavel Hamawand
a6846ad7a4 fix backbutton 2024-03-16 17:28:45 +01:00
pavel Hamawand
dc84bc997e link to unsigned reports 2024-03-16 17:25:43 +01:00
pavel Hamawand
554cf9b12e remove backbutton 2024-03-16 17:20:14 +01:00
Davenludd
7fb220f768 Refactor Register component to use InputField component 2024-03-16 15:14:03 +01:00
Mattias
d71752ad6f Created new type for timereport 2024-03-16 13:11:39 +01:00
Mattias
87c044b5bf Submit-button has been moved to the timereport 2024-03-16 13:11:26 +01:00
Mattias
966f8540df Created groundwork for the timereport 2024-03-16 13:10:46 +01:00
Mattias
d227ffc6ae Added the register component to content 2024-03-16 02:47:39 +01:00
Mattias
a67c44564f Visual fixes 2024-03-16 02:47:17 +01:00
Mattias
148af4e499 Removed duplicate path 2024-03-16 02:46:58 +01:00
Peter KW
9ff3e9314c Small change to header; uses useLocation() to get correct name 2024-03-16 02:27:33 +01:00
Peter KW
edf503776a Changed design a little so that it uses the projectlist component, added some fake projects 2024-03-16 02:23:52 +01:00
Peter KW
4a864e1ab5 Finished design and added fake users for testing 2024-03-16 02:20:36 +01:00
Peter KW
8fdd6c0b1f Finished basic view for this page 2024-03-16 02:18:28 +01:00
Peter KW
466c25a7c2 A component for listing an array of users where every user gets a link, made for AdminManageUsers.tsx 2024-03-16 02:16:06 +01:00
Peter KW
f963ca6ae5 A component for listing an array of projects where every project gets a link, made for YourProjectsPage.tsx 2024-03-16 02:15:39 +01:00
Peter KW
f0fc465d1a Merge remote-tracking branch 'origin/frontend' into gruppPP 2024-03-15 16:54:26 +01:00
pavel Hamawand
2da40e1f54 change back button UserViewTimeReportsPage 2024-03-15 15:32:06 +01:00
pavel Hamawand
8430d88a07 change back button UserProjectPage 2024-03-15 15:28:35 +01:00
pavel Hamawand
a26499bde9 change backbutton UserEditTimeReport and UserNewTimeReport 2024-03-15 15:22:55 +01:00
Peter KW
6789cc97ce Added back button to page 2024-03-15 14:57:49 +01:00
Peter KW
44aea9b765 Made a back button component since we will have one on almost every page 2024-03-15 14:51:46 +01:00
pavel Hamawand
69b0318eb0 remove edit button and fix back button 2024-03-15 14:48:40 +01:00
pavel Hamawand
dc902855f4 add edit button to viewTimeReport 2024-03-15 04:42:38 +01:00
pavel Hamawand
901bcd39c5 link to viewTimeReport 2024-03-15 04:42:38 +01:00
Peter KW
cd1dbb494c Small fix to links in admin-menu page 2024-03-15 02:29:25 +01:00
Peter KW
2ad7146588 Added some functionality to login page. Checks username + password and compares with "fake" users to determine which page to get 2024-03-15 02:28:28 +01:00
pavel Hamawand
ef28e1743e links to all projects 2024-03-15 02:03:22 +01:00
pavel Hamawand
92cf36d178 increased responsiveness - outer div 2024-03-15 01:42:43 +01:00
44 changed files with 1246 additions and 719 deletions

View file

@ -2,7 +2,6 @@ package database
import (
"embed"
"os"
"path/filepath"
"ttime/internal/types"
@ -19,7 +18,7 @@ type Database interface {
PromoteToAdmin(username string) error
GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error
Migrate(dirname string) error
Migrate() error
GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error
@ -30,6 +29,7 @@ type Database interface {
GetAllProjects() ([]types.Project, error)
GetProject(projectId int) (types.Project, error)
GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
}
// This struct is a wrapper type that holds the database connection
@ -257,15 +257,45 @@ func (d *Db) GetAllUsersApplication() ([]string, error) {
return usernames, nil
}
func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) {
var report types.WeeklyReport
query := `
SELECT
report_id,
user_id,
project_id,
week,
development_time,
meeting_time,
admin_time,
own_work_time,
study_time,
testing_time
FROM
weekly_reports
WHERE
user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
AND week = ?
`
err := d.Get(&report, query, username, projectName, week)
return report, err
}
// Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory
func (d *Db) Migrate(dirname string) error {
func (d *Db) Migrate() error {
// Read the embedded scripts directory
files, err := scripts.ReadDir("migrations")
if err != nil {
return err
}
if len(files) == 0 {
println("No migration files found")
return nil
}
tr := d.MustBegin()
// Iterate over each SQL file and execute it
@ -275,8 +305,7 @@ func (d *Db) Migrate(dirname string) error {
}
// This is perhaps not the most elegant way to do this
sqlFile := filepath.Join("migrations", file.Name())
sqlBytes, err := os.ReadFile(sqlFile)
sqlBytes, err := scripts.ReadFile("migrations/" + file.Name())
if err != nil {
return err
}

View file

@ -8,7 +8,7 @@ import (
func setupState() (Database, error) {
db := DbConnect(":memory:")
err := db.Migrate("../../migrations")
err := db.Migrate()
if err != nil {
return nil, err
}
@ -371,3 +371,42 @@ func TestAddProject(t *testing.T) {
t.Error("Added project not found")
}
}
func TestGetWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Check if the retrieved report matches the expected values
if report.UserId != 1 {
t.Errorf("Expected UserId to be 1, got %d", report.UserId)
}
if report.ProjectId != 1 {
t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId)
}
if report.Week != 1 {
t.Errorf("Expected Week to be 1, got %d", report.Week)
}
// Check other fields similarly
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,98 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// CreateProject is a simple handler that creates a new project
func (gs *GState) CreateProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
p := new(types.NewProject)
if err := c.BodyParser(p); err != nil {
return c.Status(400).SendString(err.Error())
}
// Get the username from the token and set it as the owner of the project
// This is ugly but
claims := user.Claims.(jwt.MapClaims)
owner := claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}
// GetUserProjects returns all projects that the user is a member of
func (gs *GState) GetUserProjects(c *fiber.Ctx) error {
// First we get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Then dip into the database to get the projects
projects, err := gs.Db.GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}
// ProjectRoleChange is a handler that changes a user's role within a project
func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
username := c.Params("username")
projectName := c.Params("projectName")
role := c.Params("role")
// Change the user's role within the project in the database
if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}
// GetProject retrieves a specific project by its ID
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
return c.JSON(project)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -0,0 +1,62 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil {
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 {
return c.Status(400).SendString("Invalid week number")
}
if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 {
return c.Status(400).SendString("Invalid time report")
}
if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Time report added")
}
// Handler for retrieving weekly report
func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name and week from query parameters
projectName := c.Query("projectName")
week := c.Query("week")
// Convert week to integer
weekInt, err := strconv.Atoi(week)
if err != nil {
return c.Status(400).SendString("Invalid week number")
}
// Call the database function to get the weekly report
report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return the retrieved weekly report
return c.JSON(report)
}

View file

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

View file

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

View file

@ -43,6 +43,11 @@ func main() {
// Connect to the database
db := database.DbConnect(conf.DbPath)
// Migrate the database
if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err)
}
// Get our global state
gs := handlers.NewGlobalState(db)
// Create the server

View file

@ -0,0 +1,88 @@
import { describe, expect, test } from "@jest/globals";
import { api } from "../API/API";
import { NewUser, NewWeeklyReport } from "../Types/goTypes";
describe("API", () => {
test("registerUser", async () => {
const user: NewUser = {
username: "lol", // Add the username property
password: "lol",
};
const response = await api.registerUser(user);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty("userId");
});
test("createProject", async () => {
const project = {
name: "Project X",
description: "This is a test project",
};
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t";
const response = await api.createProject(project, token);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty("projectId");
});
test("renewToken", async () => {
const refreshToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t";
const response = await api.renewToken(refreshToken);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty("accessToken");
expect(response.data).toHaveProperty("refreshToken");
});
test("getUserProjects", async () => {
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t";
const username = "rrgumdzpmc";
const response = await api.getUserProjects(username, token);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty("projects");
});
test("submitWeeklyReport", async () => {
const report: NewWeeklyReport = {
projectName: "vtmosxssst",
week: 2,
developmentTime: 40,
meetingTime: 5,
adminTime: 2,
ownWorkTime: 10,
studyTime: 12,
testingTime: 41,
};
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t";
const response = await api.submitWeeklyReport(report, token);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty(
"message",
"Report submitted successfully",
);
});
test("login", async () => {
const user: NewUser = {
username: "rrgumdzpmc", // Add an empty string value for the username property
password: "always_same",
};
const response = await api.login(user);
expect(response.success).toBe(true);
expect(response.data).toHaveProperty("accessToken");
expect(response.data).toHaveProperty("refreshToken");
});
});

View file

@ -1,5 +1,10 @@
import { NewProject, Project } from "../Types/Project";
import { NewUser, User } from "../Types/Users";
import {
NewWeeklyReport,
NewUser,
User,
Project,
NewProject,
} from "../Types/goTypes";
// This type of pattern should be hard to misuse
interface APIResponse<T> {
@ -20,8 +25,20 @@ interface API {
project: NewProject,
token: string,
): Promise<APIResponse<Project>>;
/** Submit a weekly report */
submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<Project>>;
/** Renew the token */
renewToken(token: string): Promise<APIResponse<string>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Login */
login(NewUser: NewUser): Promise<APIResponse<string>>;
}
// Export an instance of the API
@ -117,4 +134,83 @@ export const api: API = {
return { success: false, message: "Failed to renew token" };
}
},
async getUserProjects(token: string): Promise<APIResponse<Project[]>> {
try {
const response = await fetch("/api/getUserProjects", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
} else {
const data = (await response.json()) as Project[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
}
},
async submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<Project>> {
try {
const response = await fetch("/api/submitWeeklyReport", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(project),
});
if (!response.ok) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
const data = (await response.json()) as Project;
return { success: true, data };
} catch (e) {
return {
success: false,
message: "Failed to submit weekly report",
};
}
},
async login(NewUser: NewUser): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(NewUser),
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
} else {
const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token };
}
} catch (e) {
return Promise.resolve({ success: false, message: "Failed to login" });
}
},
};

View file

@ -0,0 +1,18 @@
import { useNavigate } from "react-router-dom";
function BackButton(): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(-1);
};
return (
<button
onClick={goBack}
className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;"
>
Back
</button>
);
}
export default BackButton;

View file

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

View file

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

View file

@ -0,0 +1,35 @@
import { NewUser } from "../Types/goTypes";
function LoginCheck(props: { username: string; password: string }): number {
//Example users for testing without backend, remove when using backend
const admin: NewUser = {
username: "admin",
password: "123",
};
const pmanager: NewUser = {
username: "pmanager",
password: "123",
};
const user: NewUser = {
username: "user",
password: "123",
};
//TODO: Compare with db instead when finished
if (props.username === admin.username && props.password === admin.password) {
return 1;
} else if (
props.username === pmanager.username &&
props.password === pmanager.password
) {
return 2;
} else if (
props.username === user.username &&
props.password === user.password
) {
return 3;
}
return 0;
}
export default LoginCheck;

View file

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

View file

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

View file

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

View file

@ -1,20 +1,62 @@
function NewTimeReport(): JSX.Element {
const activities = [
"Development",
"Meeting",
"Administration",
"Own Work",
"Studies",
"Testing",
];
import { useState } from "react";
import { api } from "../API/API";
import { useNavigate } from "react-router-dom";
import Button from "./Button";
import { NewWeeklyReport } from "../Types/goTypes";
export default function NewTimeReport(): JSX.Element {
const [projectName, setProjectName] = useState<string>("projectName"); // TODO: Get from backend
const [week, setWeek] = useState<number>(NaN);
const [development, setDevelopment] = useState<number>(NaN);
const [meeting, setMeeting] = useState<number>(NaN);
const [administration, setAdministration] = useState<number>(NaN);
const [ownwork, setOwnWork] = useState<number>(NaN);
const [studies, setStudies] = useState<number>(NaN);
const [testing, setTesting] = useState<number>(NaN);
const handleNewTimeReport = async (): Promise<void> => {
const newTimeReport: NewWeeklyReport = {
projectName,
week,
developmentTime: development,
meetingTime: meeting,
adminTime: administration,
ownWorkTime: ownwork,
studyTime: studies,
testingTime: testing,
};
await Promise.resolve();
await api.submitWeeklyReport(newTimeReport, "token");
};
const navigate = useNavigate();
setProjectName("Something Reasonable"); // This should obviously not be used here
return (
<>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form
onSubmit={(e) => {
if (!week) {
alert("Please enter a week number");
e.preventDefault();
return;
}
e.preventDefault();
void handleNewTimeReport();
navigate("/project");
}}
>
<div className="flex flex-col items-center">
<input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week"
placeholder="Week"
onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]);
setWeek(weekNumber);
}}
onKeyDown={(event) => {
event.preventDefault();
}}
@ -25,21 +67,121 @@ function NewTimeReport(): JSX.Element {
<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">
Activity
</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
{activities.map((activity, index) => (
<tr key={index} className="h-[10vh]">
<td>{activity}</td>
<tr className="h-[10vh]">
<td>Development</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={development}
onChange={(e) => {
setDevelopment(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meeting}
onChange={(e) => {
setMeeting(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={administration}
onChange={(e) => {
setAdministration(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownwork}
onChange={(e) => {
setOwnWork(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studies}
onChange={(e) => {
setStudies(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testing}
onChange={(e) => {
setTesting(parseInt(e.target.value));
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
@ -48,12 +190,18 @@ function NewTimeReport(): JSX.Element {
/>
</td>
</tr>
))}
</tbody>
</table>
<Button
text="Submit"
onClick={(): void => {
return;
}}
type="submit"
/>
</div>
</form>
</div>
</>
);
}
export default NewTimeReport;

View file

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

View file

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

View file

@ -1,25 +1,38 @@
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import BackButton from "../../Components/BackButton";
import { UserListAdmin } from "../../Components/UserListAdmin";
import { User } from "../../Types/Users";
import { useNavigate } from "react-router-dom";
function AdminManageUsers(): JSX.Element {
const content = <></>;
//TODO: Change so that it reads users from database
const users: User[] = [];
for (let i = 1; i <= 20; i++) {
users.push({ id: i, userName: "Example User " + i });
}
const navigate = useNavigate();
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Users</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<UserListAdmin users={users} />
</div>
</>
);
const buttons = (
<>
<Button
text="Add User"
onClick={(): void => {
return;
}}
type="button"
/>
<Button
text="Back"
onClick={(): void => {
return;
navigate("/admin-add-user");
}}
type="button"
/>
<BackButton />
</>
);

View file

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

View file

@ -1,8 +1,17 @@
import { useLocation } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import BackButton from "../../Components/BackButton";
function AdminViewUserInfo(): JSX.Element {
const content = <></>;
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<p>Put relevant info on user from database here</p>
</div>
</>
);
const buttons = (
<>
@ -13,13 +22,7 @@ function AdminViewUserInfo(): JSX.Element {
}}
type="button"
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,39 +1,36 @@
import { Link } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import { JSX } from "react/jsx-runtime";
function PMProjectPage(): JSX.Element {
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">ProjectNameExample</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[5vh] p-[30px]">
<Link to="/project-page">
<h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports
</h1>
</Link>
<Link to="/new-time-report">
<h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report
</h1>
</Link>
<Link to="/project-members">
<h1 className="font-bold underline text-[30px] cursor-pointer">
Statistics
</h1>
</Link>
<Link to="/PM-unsigned-reports">
<h1 className="font-bold underline text-[30px] cursor-pointer">
Unsigned Time Reports
</h1>
</Link>
</div>
</>
);
const buttons = (
<>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</>
);
return <BasicWindow username="Admin" content={content} buttons={buttons} />;
return <BasicWindow username="Admin" content={content} buttons={undefined} />;
}
export default PMProjectPage;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import NewTimeReport from "../../Components/TimeReport";
import BackButton from "../../Components/BackButton";
function UserEditTimeReportPage(): JSX.Element {
const content = (
@ -19,13 +20,7 @@ function UserEditTimeReportPage(): JSX.Element {
}}
type="button"
/>
<Button
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
<BackButton />
</>
);

View file

@ -13,13 +13,6 @@ function UserNewTimeReportPage(): JSX.Element {
const buttons = (
<>
<Button
text="Submit"
onClick={(): void => {
return;
}}
type="button"
/>
<Link to="/project">
<Button
text="Back"

View file

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

View file

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

View file

@ -1,25 +1,25 @@
import { Link } from "react-router-dom";
import BasicWindow from "../Components/BasicWindow";
import { ProjectListUser } from "../Components/ProjectListUser";
import { Project } from "../Types/Project";
function YourProjectsPage(): JSX.Element {
//TODO: Change so that it reads projects from database
const projects: Project[] = [];
for (let i = 1; i <= 20; i++) {
projects.push({
id: i,
name: "Example Project " + i,
description: "good",
created: "now",
owner: "me",
});
}
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Your Projects</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-between min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10px] p-[30px]">
<Link to="/project">
<h1 className="underline text-[24px] cursor-pointer font-bold">
ProjectNameExample
</h1>
</Link>
<h1 className="underline text-[24px] cursor-pointer font-bold">
ProjectNameExample2
</h1>
<h1 className="underline text-[24px] cursor-pointer font-bold">
ProjectNameExample3
</h1>
<h1 className="underline text-[24px] cursor-pointer font-bold">
ProjectNameExample4
</h1>
<div 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]">
<ProjectListUser projects={projects} />
</div>
</>
);

View file

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

View file

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

View file

@ -2,153 +2,24 @@ import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import LoginPage from "./Pages/LoginPage.tsx";
import YourProjectsPage from "./Pages/YourProjectsPage.tsx";
import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx";
import Register from "./Components/Register.tsx";
import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx";
import UserEditTimeReportPage from "./Pages/UserPages/UserEditTimeReportPage.tsx";
import UserNewTimeReportPage from "./Pages/UserPages/UserNewTimeReportPage.tsx";
import UserViewTimeReportsPage from "./Pages/UserPages/UserViewTimeReportsPage.tsx";
import PMChangeRole from "./Pages/ProjectManagerPages/PMChangeRole.tsx";
import PMOtherUsersTR from "./Pages/ProjectManagerPages/PMOtherUsersTR.tsx";
import PMProjectMembers from "./Pages/ProjectManagerPages/PMProjectMembers.tsx";
import PMProjectPage from "./Pages/ProjectManagerPages/PMProjectPage.tsx";
import PMTotalTimeActivity from "./Pages/ProjectManagerPages/PMTotalTimeActivity.tsx";
import PMTotalTimeRole from "./Pages/ProjectManagerPages/PMTotalTimeRole.tsx";
import PMUnsignedReports from "./Pages/ProjectManagerPages/PMUnsignedReports.tsx";
import PMViewUnsignedReport from "./Pages/ProjectManagerPages/PMViewUnsignedReport.tsx";
import AdminManageUsers from "./Pages/AdminPages/AdminManageUsers.tsx";
import AdminViewUserInfo from "./Pages/AdminPages/AdminViewUserInfo.tsx";
import AdminManageProjects from "./Pages/AdminPages/AdminManageProjects.tsx";
import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx";
import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx";
import AdminChangeUsername from "./Pages/AdminPages/AdminChangeUsername.tsx";
import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx";
import AdminProjectChangeUserRole from "./Pages/AdminPages/AdminProjectChangeUserRole.tsx";
import AdminProjectManageMembers from "./Pages/AdminPages/AdminProjectManageMembers.tsx";
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx";
import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx";
import App from "./Pages/App";
import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage";
import YourProjectsPage from "./Pages/YourProjectsPage";
// This is where the routes are mounted
const router = createBrowserRouter([
{
path: "/",
element: <LoginPage />,
element: <App />,
},
{
path: "/your-projects",
path: "/admin",
element: <AdminMenuPage />,
},
{
path: "/pm",
element: <YourProjectsPage />,
},
{
path: "/edit-time-report",
element: <UserEditTimeReportPage />,
},
{
path: "/new-time-report",
element: <UserNewTimeReportPage />,
},
{
path: "/project",
element: <UserProjectPage />,
},
{
path: "/register",
element: <Register />,
},
{
path: "/admin-menu",
element: <AdminMenuPage />,
},
{
path: "/project-page",
element: <UserViewTimeReportsPage />,
},
{
path: "/change-role",
element: <PMChangeRole />,
},
{
path: "/other-users-time-reports",
element: <PMOtherUsersTR />,
},
{
path: "/project-members",
element: <PMProjectMembers />,
},
{
path: "/PM-project-page",
element: <PMProjectPage />,
},
{
path: "/PM-time-activity",
element: <PMTotalTimeActivity />,
},
{
path: "/PM-time-role",
element: <PMTotalTimeRole />,
},
{
path: "/PM-unsigned-reports",
element: <PMUnsignedReports />,
},
{
path: "/PM-view-unsigned-report",
element: <PMViewUnsignedReport />,
},
{
path: "/admin-add-project",
element: <AdminAddProject />,
},
{
path: "/admin-add-user",
element: <AdminAddUser />,
},
{
path: "/admin-change-username",
element: <AdminChangeUsername />,
},
{
path: "/admin-manage-projects",
element: <AdminManageProjects />,
},
{
path: "/admin-manage-users",
element: <AdminManageUsers />,
},
{
path: "/admin-menu",
element: <AdminMenuPage />,
},
{
path: "/admin-project-add-member",
element: <AdminProjectAddMember />,
},
{
path: "/admin-project-change-user-role",
element: <AdminProjectChangeUserRole />,
},
{
path: "/admin-project-manage-members",
element: <AdminProjectManageMembers />,
},
{
path: "/admin-project-page",
element: <AdminProjectPage />,
},
{
path: "/admin-project-statistics",
element: <AdminProjectStatistics />,
},
{
path: "/admin-project-view-members",
element: <AdminProjectViewMemberInfo />,
},
{
path: "/admin-view-user",
element: <AdminViewUserInfo />,
},
]);
// Semi-hacky way to get the root element