From 67a400188383340d6a6e56996c65ee2444044d3b Mon Sep 17 00:00:00 2001 From: z1glr Date: Tue, 21 Jan 2025 09:41:27 +0000 Subject: [PATCH] fixed assignments view once again --- backend/pkg/db/assignments/assignments.go | 24 +-- .../pkg/db/availabilities/availabilities.go | 14 +- .../db/availabilities/userAvailabilities.go | 2 +- backend/pkg/db/db.go | 2 + backend/pkg/db/events/events.go | 5 +- backend/pkg/db/tasks/tasks.go | 14 +- backend/pkg/router/availabilities.go | 37 +++- backend/pkg/router/events.go | 29 ++- backend/pkg/router/router.go | 8 +- backend/pkg/router/tasks.go | 58 +++-- backend/setup.sql | 10 +- client/package-lock.json | 116 +++++----- client/src/Zustand.ts | 20 +- client/src/app/Main.tsx | 2 +- .../admin/(availabilities)/Availabilities.tsx | 123 ++++++----- .../(availabilities)/AvailabilityEditor.tsx | 25 ++- .../(availabilities)/EditAvailability.tsx | 7 +- client/src/app/admin/(tasks)/AddTask.tsx | 4 +- client/src/app/admin/(tasks)/EditTask.tsx | 4 +- client/src/app/admin/(tasks)/TaskEditor.tsx | 23 +- client/src/app/admin/(tasks)/Tasks.tsx | 88 ++++++-- client/src/app/assignments/page.tsx | 59 +++--- client/src/components/AvailabilityChip.tsx | 23 ++ client/src/components/DeleteConfirmation.tsx | 49 +++++ client/src/components/Event/AddEvent.tsx | 9 +- client/src/components/Event/EditEvent.tsx | 199 ++---------------- client/src/components/Event/EventEditor.tsx | 131 ++++++++++++ client/src/components/LocalDate.tsx | 6 +- client/src/lib.ts | 53 ++++- 29 files changed, 695 insertions(+), 449 deletions(-) create mode 100644 client/src/components/AvailabilityChip.tsx create mode 100644 client/src/components/DeleteConfirmation.tsx create mode 100644 client/src/components/Event/EventEditor.tsx diff --git a/backend/pkg/db/assignments/assignments.go b/backend/pkg/db/assignments/assignments.go index 11c8b35..cd8ba80 100644 --- a/backend/pkg/db/assignments/assignments.go +++ b/backend/pkg/db/assignments/assignments.go @@ -4,27 +4,19 @@ import ( "github.com/johannesbuehl/golunteer/backend/pkg/db" ) -type assignments map[string]*string - -type eventAssignmentDB struct { - TaskName string `db:"taskName"` - UserName *string `db:"userName"` +type EventAssignment struct { + TaskID int `db:"taskID" json:"taskID"` + TaskName string `db:"taskName" json:"taskName"` + UserName *string `db:"userName" json:"userName"` } -func Event(eventID int) (assignments, error) { +func Event(eventID int) ([]EventAssignment, error) { // get the assignments from the database - var assignmentRows []eventAssignmentDB + var assignmentRows []EventAssignment - if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS LEFT JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil { + if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, taskID, TASKS.name AS taskName FROM USER_ASSIGNMENTS LEFT JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil { return nil, err } else { - // transform the rows into the returned map - eventAssignments := assignments{} - - for _, assignment := range assignmentRows { - eventAssignments[assignment.TaskName] = assignment.UserName - } - - return eventAssignments, nil + return assignmentRows, nil } } diff --git a/backend/pkg/db/availabilities/availabilities.go b/backend/pkg/db/availabilities/availabilities.go index 91858ae..546e63d 100644 --- a/backend/pkg/db/availabilities/availabilities.go +++ b/backend/pkg/db/availabilities/availabilities.go @@ -10,19 +10,19 @@ type AvailabilityDB struct { } type Availability struct { - Text string `db:"text" json:"text" validate:"required"` + Name string `db:"name" json:"name" validate:"required"` Enabled bool `db:"enabled" json:"enabled" validate:"required"` Color string `db:"color" json:"color" validate:"required"` } func Add(a Availability) error { - _, err := db.DB.NamedExec("INSERT INTO AVAILABILITIES (text, color, enabled) VALUES (:text, :color, :enabled)", a) + _, err := db.DB.NamedExec("INSERT INTO AVAILABILITIES (name, color, enabled) VALUES (:name, :color, :enabled)", a) return err } func Update(a AvailabilityDB) error { - _, err := db.DB.NamedExec("UPDATE AVAILABILITIES SET text = :text, color = :color, enabled = :enabled WHERE id = :id", a) + _, err := db.DB.NamedExec("UPDATE AVAILABILITIES SET name = :name, color = :color, enabled = :enabled WHERE id = :id", a) return err } @@ -47,7 +47,7 @@ func Keys() (map[int]Availability, error) { for _, a := range availabilitiesRaw { availabilities[a.Id] = Availability{ - Text: a.Text, + Name: a.Name, Enabled: a.Enabled, Color: a.Color, } @@ -56,3 +56,9 @@ func Keys() (map[int]Availability, error) { return availabilities, nil } } + +func Delete(id int) error { + _, err := db.DB.Exec("DELETE FROM AVAILABILITIES WHERE id = $1", id) + + return err +} diff --git a/backend/pkg/db/availabilities/userAvailabilities.go b/backend/pkg/db/availabilities/userAvailabilities.go index 1f9e3d5..3c20082 100644 --- a/backend/pkg/db/availabilities/userAvailabilities.go +++ b/backend/pkg/db/availabilities/userAvailabilities.go @@ -24,7 +24,7 @@ func Event(eventID int) (map[string]string, error) { return nil, err } else { for _, a := range availabilitiesRows { - eventAvailabilities[a.UserName] = availabilitiesMap[a.AvailabilityID].Text + eventAvailabilities[a.UserName] = availabilitiesMap[a.AvailabilityID].Name } return eventAvailabilities, nil diff --git a/backend/pkg/db/db.go b/backend/pkg/db/db.go index 408c36f..71eba28 100644 --- a/backend/pkg/db/db.go +++ b/backend/pkg/db/db.go @@ -19,6 +19,8 @@ func init() { // connect to the database DB = sqlx.MustOpen("sqlite", config.Database) + DB.MustExec("PRAGMA foreign_keys = ON") + // create the tables if they don't exist if dbSetupInstructions, err := os.ReadFile("setup.sql"); err != nil { panic("can't read database-setup") diff --git a/backend/pkg/db/events/events.go b/backend/pkg/db/events/events.go index 163f5b7..050958a 100644 --- a/backend/pkg/db/events/events.go +++ b/backend/pkg/db/events/events.go @@ -11,7 +11,7 @@ import ( type EventWithAssignment struct { eventDataDB - Tasks map[string]*string `json:"tasks"` + Tasks []assignments.EventAssignment `json:"tasks"` } type EventWithAvailabilities struct { @@ -61,6 +61,7 @@ type EventCreate struct { } func Create(event EventCreate) error { + // convert the date to utc if result, err := db.DB.NamedExec("INSERT INTO EVENTS (date, description) VALUES (:date, :description)", event); err != nil { return err } else if id, err := result.LastInsertId(); err != nil { @@ -160,7 +161,7 @@ func Update(event EventPatch) error { func All() ([]eventDataDB, error) { var dbRows []eventDataDB - if err := db.DB.Select(&dbRows, "SELECT *, DATE_FORMAT(date, '%Y-%m-%dT%H:%i:%s') as date FROM EVENTS"); err != nil { + if err := db.DB.Select(&dbRows, "SELECT * FROM EVENTS"); err != nil { return nil, err } else { return dbRows, nil diff --git a/backend/pkg/db/tasks/tasks.go b/backend/pkg/db/tasks/tasks.go index c8d651b..9a69ced 100644 --- a/backend/pkg/db/tasks/tasks.go +++ b/backend/pkg/db/tasks/tasks.go @@ -10,7 +10,7 @@ type TaskDB struct { } type Task struct { - Text string `json:"text" db:"text" validate:"required"` + Name string `json:"name" db:"name" validate:"required"` Enabled bool `json:"enabled" db:"enabled" validate:"required"` } @@ -34,7 +34,7 @@ func GetMap() (map[int]Task, error) { for _, a := range tasksRaw { tasks[a.ID] = Task{ - Text: a.Text, + Name: a.Name, Enabled: a.Enabled, } } @@ -44,13 +44,19 @@ func GetMap() (map[int]Task, error) { } func Add(t Task) error { - _, err := db.DB.NamedExec("INSERT INTO TASKS (text, enabled) VALUES (:text, :enabled)", &t) + _, err := db.DB.NamedExec("INSERT INTO TASKS (name, enabled) VALUES (:name, :enabled)", &t) return err } func Update(t TaskDB) error { - _, err := db.DB.NamedExec("UPDATE TASKS set text = :text, enabled = :enabled WHERE id = :id", &t) + _, err := db.DB.NamedExec("UPDATE TASKS set name = :name, enabled = :enabled WHERE id = :id", &t) + + return err +} + +func Delete(i int) error { + _, err := db.DB.Exec("DELETE FROM TASKS WHERE id = $1", i) return err } diff --git a/backend/pkg/router/availabilities.go b/backend/pkg/router/availabilities.go index b2665cb..6d7ddb8 100644 --- a/backend/pkg/router/availabilities.go +++ b/backend/pkg/router/availabilities.go @@ -16,15 +16,13 @@ func getAvailabilities(args HandlerArgs) responseMessage { return response } else { - response.Data = struct { - Availabilities []availabilities.AvailabilityDB `json:"availabilities"` - }{Availabilities: avails} + response.Data = avails return response } } -func postAvailabilitie(args HandlerArgs) responseMessage { +func postAvailability(args HandlerArgs) responseMessage { response := responseMessage{} // check admin @@ -65,7 +63,7 @@ func postAvailabilitie(args HandlerArgs) responseMessage { } } -func patchAvailabilities(args HandlerArgs) responseMessage { +func patchAvailabilitiy(args HandlerArgs) responseMessage { response := responseMessage{} // check admin @@ -105,3 +103,32 @@ func patchAvailabilities(args HandlerArgs) responseMessage { } } } + +func deleteAvailability(args HandlerArgs) responseMessage { + // check admin + if !args.User.Admin { + logger.Warn().Msg("availability-deletion failed: user is no admin") + + return responseMessage{ + Status: fiber.StatusUnauthorized, + } + + // parse the query + } else if taskID := args.C.QueryInt("id", -1); taskID == -1 { + logger.Log().Msg("availability-deletion failed: invalid query: doesn't include \"id\"") + + return responseMessage{ + Status: fiber.StatusBadRequest, + } + + // delete the task from the database + } else if err := availabilities.Delete(taskID); err != nil { + logger.Error().Msgf("availability-deletion failed: can't delete task from database: %v", err) + + return responseMessage{ + Status: fiber.StatusInternalServerError, + } + } else { + return responseMessage{} + } +} diff --git a/backend/pkg/router/events.go b/backend/pkg/router/events.go index 1e63baf..5ebb9c6 100644 --- a/backend/pkg/router/events.go +++ b/backend/pkg/router/events.go @@ -118,18 +118,31 @@ func getEventsUserPending(args HandlerArgs) responseMessage { } func deleteEvent(args HandlerArgs) responseMessage { - response := responseMessage{} - // check for admin if !args.User.Admin { - response.Status = fiber.StatusForbidden + logger.Warn().Msg("event-delete failed: user is no admin") + + return responseMessage{ + Status: fiber.StatusForbidden, + } // -1 can't be valid } else if eventId := args.C.QueryInt("id", -1); eventId == -1 { - response.Status = fiber.StatusBadRequest - } else if err := events.Delete(eventId); err != nil { - response.Status = fiber.StatusInternalServerError - } + logger.Log().Msgf("event-delete failed: \"id\" is missing in query") - return response + return responseMessage{ + Status: fiber.StatusBadRequest, + } + } else if err := events.Delete(eventId); err != nil { + + logger.Error().Msgf("event-delete failed: can't delete from database: %v", err) + + return responseMessage{ + Status: fiber.StatusInternalServerError, + } + } else { + logger.Log().Msgf("deleted event with id %d", eventId) + + return responseMessage{} + } } diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go index d6c3a48..0d733d7 100644 --- a/backend/pkg/router/router.go +++ b/backend/pkg/router/router.go @@ -86,20 +86,22 @@ func init() { "POST": { "events": postEvent, "users": postUser, - "availabilities": postAvailabilitie, + "availabilities": postAvailability, "tasks": postTask, }, "PATCH": { "users": patchUser, "events": patchEvent, - "availabilities": patchAvailabilities, + "availabilities": patchAvailabilitiy, "tasks": patchTask, }, "PUT": { "users/password": putPassword, }, "DELETE": { - "event": deleteEvent, + "event": deleteEvent, + "tasks": deleteTask, + "availabilities": deleteAvailability, }, } diff --git a/backend/pkg/router/tasks.go b/backend/pkg/router/tasks.go index 0fc0e73..dd6865c 100644 --- a/backend/pkg/router/tasks.go +++ b/backend/pkg/router/tasks.go @@ -6,32 +6,15 @@ import ( ) func getTasks(args HandlerArgs) responseMessage { - // check wether the "map"-query is given - if args.C.QueryBool("map") { - if tasks, err := tasks.GetMap(); err != nil { - logger.Error().Msgf("can't get tasks: %v", err) + if taskSlice, err := tasks.GetSlice(); err != nil { + logger.Error().Msgf("can't get tasks: %v", err) - return responseMessage{ - Status: fiber.StatusInternalServerError, - } - } else { - return responseMessage{ - Data: tasks, - } + return responseMessage{ + Status: fiber.StatusInternalServerError, } } else { - if taskSlice, err := tasks.GetSlice(); err != nil { - logger.Error().Msgf("can't get tasks: %v", err) - - return responseMessage{ - Status: fiber.StatusInternalServerError, - } - } else { - return responseMessage{ - Data: struct { - Tasks []tasks.TaskDB `json:"tasks"` - }{Tasks: taskSlice}, - } + return responseMessage{ + Data: taskSlice, } } } @@ -115,3 +98,32 @@ func patchTask(args HandlerArgs) responseMessage { } } } + +func deleteTask(args HandlerArgs) responseMessage { + // check admin + if !args.User.Admin { + logger.Warn().Msg("task-deletion failed: user is no admin") + + return responseMessage{ + Status: fiber.StatusUnauthorized, + } + + // parse the query + } else if taskID := args.C.QueryInt("id", -1); taskID == -1 { + logger.Log().Msg("task-deletion failed: invalid query: doesn't include \"id\"") + + return responseMessage{ + Status: fiber.StatusBadRequest, + } + + // delete the task from the database + } else if err := tasks.Delete(taskID); err != nil { + logger.Error().Msgf("task-deletion failed: can't delete task from database: %v", err) + + return responseMessage{ + Status: fiber.StatusInternalServerError, + } + } else { + return responseMessage{} + } +} diff --git a/backend/setup.sql b/backend/setup.sql index 6451d16..4b6745c 100644 --- a/backend/setup.sql +++ b/backend/setup.sql @@ -1,19 +1,19 @@ CREATE TABLE IF NOT EXISTS TASKS ( id INTEGER PRIMARY KEY, - text varchar(64) NOT NULL, - enabled BOOL DEFAULT(true) + name varchar(64) NOT NULL, + enabled BOOL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS AVAILABILITIES ( id INTEGER PRIMARY KEY, - text varchar(32) NOT NULL, + name varchar(32) NOT NULL, color varchar(7) NOT NULL, - enabled BOOL DEFAULT(true) + enabled BOOL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS USERS ( name varchar(64) PRIMARY KEY, - password binary(60) NOT NULL, + password BLOB NOT NULL, admin BOOL NOT NULL DEFAULT(false), tokenID varchar(64) NOT NULL, CHECK (length(password) = 60), diff --git a/client/package-lock.json b/client/package-lock.json index 11a3d53..3edbf32 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -244,9 +244,9 @@ } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.10.0.tgz", - "integrity": "sha512-PDeky6nDAyHYEtmSi2X1PG9YpqE+2BRTJT7JvPix8K8JX1wBWQNao6KcPtmZpttQHUHmzMcd/rne7lFesSzUKQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz", + "integrity": "sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw==", "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "2.3.2", @@ -4652,17 +4652,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", - "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/type-utils": "8.20.0", - "@typescript-eslint/utils": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4682,16 +4682,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", - "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4" }, "engines": { @@ -4707,14 +4707,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4725,14 +4725,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", - "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.0" }, @@ -4749,9 +4749,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", "dev": true, "license": "MIT", "engines": { @@ -4763,14 +4763,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4846,16 +4846,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4870,13 +4870,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5318,9 +5318,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001692", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", - "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "funding": [ { "type": "opencollective", @@ -6519,9 +6519,9 @@ } }, "node_modules/framer-motion": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.1.tgz", - "integrity": "sha512-EQa8c9lWVOm4zlz14MsBJWr8woq87HsNmsBnQNvcS0hs8uzw6HtGAxZyIU7EGTVpHD1C1n01ufxRyarXcNzpPg==", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "license": "MIT", "dependencies": { "motion-dom": "^11.18.1", @@ -6657,9 +6657,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.9.0.tgz", + "integrity": "sha512-52n24W52sIueosRe0XZ8Ex5Yle+WbhfCKnV/gWXpbVR8FXNTfqdKEKUSypKso66VRHTvvcQxL44UTZbJRlCTnw==", "dev": true, "license": "MIT", "dependencies": { @@ -6938,14 +6938,14 @@ } }, "node_modules/intl-messageformat": { - "version": "10.7.12", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.12.tgz", - "integrity": "sha512-4HBsPDJ61jZwNikauvm0mcLvs1AfCBbihiqOX2AGs1MX7SA1H0SNKJRSWxpZpToGoNzvoYLsJJ2pURkbEDg+Dw==", + "version": "10.7.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.14.tgz", + "integrity": "sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ==", "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "2.3.2", "@formatjs/fast-memoize": "2.2.6", - "@formatjs/icu-messageformat-parser": "2.10.0", + "@formatjs/icu-messageformat-parser": "2.11.0", "tslib": "2" } }, diff --git a/client/src/Zustand.ts b/client/src/Zustand.ts index 32aafbd..c01ae3e 100644 --- a/client/src/Zustand.ts +++ b/client/src/Zustand.ts @@ -2,11 +2,13 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { Task } from "./lib"; +import { Availability } from "./app/admin/(availabilities)/AvailabilityEditor"; export interface EventData { id: number; date: string; - tasks: Partial>; + tasks: { taskID: number; taskName: string; userName: string | null }[]; description: string; } @@ -17,6 +19,9 @@ export interface User { interface Zustand { user: User | null; + tasks?: Task[]; + availabilities?: Availability[]; + patch: (zustand?: Partial) => void; reset: (zustand?: Partial) => void; } @@ -26,19 +31,24 @@ const initialState = { const zustand = create()( persist( - (set) => ({ + (set, get) => ({ ...initialState, - reset: (newZustand) => + reset: (newZustand) => { + console.debug("reset"); set({ ...initialState, ...newZustand, - }), + }); + }, + patch: (patch) => set({ ...get(), ...patch }), }), { name: "golunteer-storage", partialize: (state) => Object.fromEntries( - Object.entries(state).filter(([key]) => ["user"].includes(key)), + Object.entries(state).filter(([key]) => + ["user", "tasksList", "tasksMap"].includes(key), + ), ), }, ), diff --git a/client/src/app/Main.tsx b/client/src/app/Main.tsx index 7d39e60..d90d7d3 100644 --- a/client/src/app/Main.tsx +++ b/client/src/app/Main.tsx @@ -33,7 +33,7 @@ export default function Main({ children }: { children: React.ReactNode }) { const response = await welcomeResult.json(); if (response.userName !== undefined && response.userName !== "") { - zustand.getState().reset({ user: response }); + zustand.getState().patch({ user: response }); loggedIn = true; } diff --git a/client/src/app/admin/(availabilities)/Availabilities.tsx b/client/src/app/admin/(availabilities)/Availabilities.tsx index 515c491..8658a37 100644 --- a/client/src/app/admin/(availabilities)/Availabilities.tsx +++ b/client/src/app/admin/(availabilities)/Availabilities.tsx @@ -1,10 +1,10 @@ -import { color2Tailwind, colors } from "@/components/Colorselector"; -import { apiCall } from "@/lib"; -import { AddLarge, Edit } from "@carbon/icons-react"; +import { colors } from "@/components/Colorselector"; +import { apiCall, getAvailabilities } from "@/lib"; +import { AddLarge, Edit, TrashCan } from "@carbon/icons-react"; import { Button, + ButtonGroup, Checkbox, - Chip, Table, TableBody, TableCell, @@ -18,26 +18,20 @@ import { useState } from "react"; import AddAvailability from "./AddAvailability"; import { Availability } from "./AvailabilityEditor"; import EditAvailability from "./EditAvailability"; +import DeleteConfirmation from "@/components/DeleteConfirmation"; +import AvailabilityChip from "@/components/AvailabilityChip"; +import zustand from "@/Zustand"; export default function Availabilities() { const [showAddAvailability, setShowAddAvailability] = useState(false); const [editAvailability, setEditAvailability] = useState(); + const [deleteAvailability, setDeleteAvailability] = useState(); const availabilities = useAsyncList({ async load() { - const result = await apiCall("GET", "availabilities"); - - if (result.ok) { - const json = await result.json(); - - return { - items: json.availabilities, - }; - } else { - return { - items: [], - }; - } + return { + items: await getAvailabilities(), + }; }, async sort({ items, sortDescriptor }) { return { @@ -46,7 +40,7 @@ export default function Availabilities() { switch (sortDescriptor.column) { case "text": - cmp = a.text.localeCompare(b.text); + cmp = a.name.localeCompare(b.name); break; case "enabled": if (a.enabled && !b.enabled) { @@ -76,6 +70,26 @@ export default function Availabilities() { }, }); + function reload() { + // clear the availabilites in the zustand + zustand.getState().patch({ availabilities: undefined }); + + // refresh the availabilites + availabilities.reload(); + } + + async function sendDeleteAvailability(id: number | undefined) { + if (id !== undefined) { + const result = await apiCall("DELETE", "availabilities", { id }); + + if (result.ok) { + reload(); + + setDeleteAvailability(undefined); + } + } + } + const topContent = ( <> + + + + )} @@ -153,7 +165,7 @@ export default function Availabilities() { !isOpen ? setEditAvailability(undefined) : null } - onSuccess={availabilities.reload} + onSuccess={reload} /> + + + !isOpen ? setDeleteAvailability(undefined) : null + } + header="Delete Availability" + onDelete={() => sendDeleteAvailability(deleteAvailability?.id)} + > + {!!deleteAvailability ? ( + <> + The availability{" "} + will be + deleted. + + ) : null} + ); } diff --git a/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx b/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx index e8c7de0..c6610c0 100644 --- a/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx +++ b/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx @@ -9,10 +9,10 @@ import { ModalFooter, ModalHeader, } from "@heroui/react"; -import React, { FormEvent, useState } from "react"; +import React, { FormEvent, useEffect, useState } from "react"; export interface Availability { - text: string; + name: string; color: string; id: number | undefined; enabled: boolean; @@ -26,13 +26,22 @@ export default function AvailabilityEditor(props: { onOpenChange?: (isOpen: boolean) => void; onSubmit?: (e: Availability) => void; }) { - const [text, setText] = useState(props.value?.text ?? ""); + const [name, setName] = useState(props.value?.name ?? ""); const [color, setColor] = useState(props.value?.color ?? "Red"); const [enabled, setEnabled] = useState(props.value?.enabled ?? true); + // clear the inputs on closing + useEffect(() => { + if (!props.isOpen) { + setName(""); + setColor(""); + setEnabled(true); + } + }, [props.isOpen]); + function submit(e: FormEvent) { const formData = Object.fromEntries(new FormData(e.currentTarget)) as { - text: string; + name: string; color: string; enabled: string; }; @@ -64,10 +73,10 @@ export default function AvailabilityEditor(props: { diff --git a/client/src/app/admin/(availabilities)/EditAvailability.tsx b/client/src/app/admin/(availabilities)/EditAvailability.tsx index 72f7f82..1c28a21 100644 --- a/client/src/app/admin/(availabilities)/EditAvailability.tsx +++ b/client/src/app/admin/(availabilities)/EditAvailability.tsx @@ -2,6 +2,7 @@ import { apiCall } from "@/lib"; import AvailabilityEditor, { Availability } from "./AvailabilityEditor"; import { Button } from "@heroui/react"; import { Renew } from "@carbon/icons-react"; +import AvailabilityChip from "@/components/AvailabilityChip"; export default function EditAvailability(props: { value: Availability | undefined; @@ -24,9 +25,9 @@ export default function EditAvailability(props: { header={ <> Edit Availability{" "} - - "{props.value?.text}" - + {!!props.value ? ( + + ) : null} } footer={ diff --git a/client/src/app/admin/(tasks)/AddTask.tsx b/client/src/app/admin/(tasks)/AddTask.tsx index 925979e..3816e39 100644 --- a/client/src/app/admin/(tasks)/AddTask.tsx +++ b/client/src/app/admin/(tasks)/AddTask.tsx @@ -1,5 +1,5 @@ -import { apiCall } from "@/lib"; -import TaskEditor, { Task } from "./TaskEditor"; +import { apiCall, Task } from "@/lib"; +import TaskEditor from "./TaskEditor"; import { Button } from "@heroui/react"; import { AddLarge } from "@carbon/icons-react"; diff --git a/client/src/app/admin/(tasks)/EditTask.tsx b/client/src/app/admin/(tasks)/EditTask.tsx index 5668f88..97c3565 100644 --- a/client/src/app/admin/(tasks)/EditTask.tsx +++ b/client/src/app/admin/(tasks)/EditTask.tsx @@ -20,12 +20,12 @@ export default function EditTask(props: { return ( Edit Task{" "} - "{props.value?.text}" + "{props.value?.name}" } diff --git a/client/src/app/admin/(tasks)/TaskEditor.tsx b/client/src/app/admin/(tasks)/TaskEditor.tsx index db29c55..eec9580 100644 --- a/client/src/app/admin/(tasks)/TaskEditor.tsx +++ b/client/src/app/admin/(tasks)/TaskEditor.tsx @@ -9,7 +9,7 @@ import { ModalFooter, ModalHeader, } from "@heroui/react"; -import React, { FormEvent, useState } from "react"; +import React, { FormEvent, useEffect, useState } from "react"; export default function TaskEditor(props: { header: React.ReactNode; @@ -19,13 +19,20 @@ export default function TaskEditor(props: { onOpenChange?: (isOpen: boolean) => void; onSubmit?: (e: Task) => void; }) { - const [text, setText] = useState(props.value?.text ?? ""); + const [name, setName] = useState(props.value?.name ?? ""); const [enabled, setEnabled] = useState(props.value?.enabled ?? true); + // clear the inputs on closing + useEffect(() => { + if (!props.isOpen) { + setName(""); + setEnabled(true); + } + }, [props.isOpen]); + function submit(e: FormEvent) { const formData = Object.fromEntries(new FormData(e.currentTarget)) as { - text: string; - color: string; + name: string; enabled: string; }; @@ -56,10 +63,10 @@ export default function TaskEditor(props: { diff --git a/client/src/app/admin/(tasks)/Tasks.tsx b/client/src/app/admin/(tasks)/Tasks.tsx index c1ba1a6..09b8910 100644 --- a/client/src/app/admin/(tasks)/Tasks.tsx +++ b/client/src/app/admin/(tasks)/Tasks.tsx @@ -1,7 +1,8 @@ import { apiCall, Task } from "@/lib"; -import { AddLarge, Edit } from "@carbon/icons-react"; +import { AddLarge, Edit, TrashCan } from "@carbon/icons-react"; import { Button, + ButtonGroup, Checkbox, Table, TableBody, @@ -15,20 +16,23 @@ import { useAsyncList } from "@react-stately/data"; import { useState } from "react"; import AddTask from "./AddTask"; import EditTask from "./EditTask"; +import DeleteConfirmation from "@/components/DeleteConfirmation"; +import zustand from "@/Zustand"; export default function Tasks() { const [showAddTask, setShowAddTask] = useState(false); const [editTask, setEditTask] = useState(); + const [deleteTask, setDeleteTask] = useState(); const tasks = useAsyncList({ async load() { const result = await apiCall("GET", "tasks"); if (result.ok) { - const json = await result.json(); + const json = (await result.json()) as Task[]; return { - items: json.tasks, + items: json, }; } else { return { @@ -43,7 +47,7 @@ export default function Tasks() { switch (sortDescriptor.column) { case "text": - cmp = a.text.localeCompare(b.text); + cmp = a.name.localeCompare(b.name); break; case "enabled": if (a.enabled && !b.enabled) { @@ -64,6 +68,25 @@ export default function Tasks() { }, }); + function reload() { + // clear the zustand + zustand.getState().patch({ tasks: undefined }); + + // reload the tasks + tasks.reload(); + } + + async function sendDeleteTask(id: number | undefined) { + if (id !== undefined) { + const result = await apiCall("DELETE", "tasks", { id }); + + if (result.ok) { + tasks.reload(); + setDeleteTask(undefined); + } + } + } + const topContent = ( <> + + + + )} @@ -125,15 +160,32 @@ export default function Tasks() { (!isOpen ? setEditTask(undefined) : null)} - onSuccess={tasks.reload} + onSuccess={reload} /> + + (!isOpen ? setDeleteTask(undefined) : null)} + header="Delete Task" + onDelete={() => sendDeleteTask(deleteTask?.id)} + > + {!!deleteTask ? ( + <> + The task{" "} + + {deleteTask.name} + {" "} + will be deleted. + + ) : null} + ); } diff --git a/client/src/app/assignments/page.tsx b/client/src/app/assignments/page.tsx index 71b0bef..c256292 100644 --- a/client/src/app/assignments/page.tsx +++ b/client/src/app/assignments/page.tsx @@ -1,9 +1,9 @@ "use client"; import AddEvent from "@/components/Event/AddEvent"; -import EditEvent, { EventSubmitData } from "@/components/Event/EditEvent"; +import EditEvent from "@/components/Event/EditEvent"; import LocalDate from "@/components/LocalDate"; -import { apiCall, getTaskMap } from "@/lib"; +import { apiCall, getTasks } from "@/lib"; import { EventData } from "@/Zustand"; import { Add, @@ -31,6 +31,7 @@ import { TableBody, TableCell, TableColumn, + TableColumnProps, TableHeader, TableRow, Tooltip, @@ -46,20 +47,30 @@ export default function AdminPanel() { const [deleteEvent, setDeleteEvent] = useState(); // get the available tasks and craft them into the headers - const headers = useAsyncList({ + const headers = useAsyncList<{ + key: string | number; + label: string; + align?: string; + }>({ async load() { - const tasks = await getTaskMap(); + const tasks = await getTasks(); - return { + const headers = { items: [ { key: "date", label: "Date" }, { key: "description", label: "Description" }, - ...Object.values(tasks) + ...tasks .filter((task) => task.enabled) - .map((task) => ({ label: task.text, key: task.text })), - { key: "actions", label: "Action" }, + .map((task) => ({ + label: task.name, + key: task.id ?? -1, + align: "center", + })), + { key: "actions", label: "Action", align: "center" }, ], }; + + return headers; }, }); @@ -72,7 +83,9 @@ export default function AdminPanel() { ); if (result.ok) { - return { items: await result.json() }; + const data = await result.json(); + + return { items: data }; } else { return { items: [] }; } @@ -145,7 +158,7 @@ export default function AdminPanel() { - + + + + + ); +} diff --git a/client/src/components/Event/AddEvent.tsx b/client/src/components/Event/AddEvent.tsx index 45c5aa7..bfeb4f8 100644 --- a/client/src/components/Event/AddEvent.tsx +++ b/client/src/components/Event/AddEvent.tsx @@ -1,7 +1,7 @@ import { Button } from "@heroui/react"; -import EditEvent, { EventSubmitData } from "./EditEvent"; import { apiCall } from "@/lib"; import { AddLarge } from "@carbon/icons-react"; +import EventEditor, { EventSubmitData } from "./EventEditor"; export default function AddEvent(props: { className?: string; @@ -20,8 +20,9 @@ export default function AddEvent(props: { } return ( - void addEvent(data)} footer={