diff --git a/backend/pkg/db/availabilites/userAvailabilities.go b/backend/pkg/db/availabilites/userAvailabilities.go deleted file mode 100644 index 7340291..0000000 --- a/backend/pkg/db/availabilites/userAvailabilities.go +++ /dev/null @@ -1,36 +0,0 @@ -package availabilites - -import ( - "github.com/johannesbuehl/golunteer/backend/pkg/db" - "github.com/johannesbuehl/golunteer/backend/pkg/db/users" -) - -type eventAvailabilites struct { - userName string `db:"userName"` - AvailabilityID int `db:"availabilityID"` -} - -func Event(eventID int) (map[string]string, error) { - // get the availabilites for the event - var availabilitesRows []eventAvailabilites - - if err := db.DB.Select(&availabilitesRows, "SELECT (userID, availabilityID) FROM USER_AVAILABILITES WHERE eventID = ?", eventID); err != nil { - return nil, err - } else { - // transform the result into a map - eventAvailabilities := map[string]string{} - - // get the availabilites - if availabilitesMap, err := Keys(); err != nil { - return nil, err - } else if usersMap, err := users.Get(); err != nil { - return nil, err - } else { - for _, a := range availabilitesRows { - eventAvailabilities[usersMap[a.userName].Name] = availabilitesMap[a.AvailabilityID].Text - } - - return eventAvailabilities, nil - } - } -} diff --git a/backend/pkg/db/availabilites/availabilities.go b/backend/pkg/db/availabilities/availabilities.go similarity index 54% rename from backend/pkg/db/availabilites/availabilities.go rename to backend/pkg/db/availabilities/availabilities.go index 7244cdf..2534ddd 100644 --- a/backend/pkg/db/availabilites/availabilities.go +++ b/backend/pkg/db/availabilities/availabilities.go @@ -1,4 +1,4 @@ -package availabilites +package availabilities import ( "fmt" @@ -8,7 +8,7 @@ import ( "github.com/johannesbuehl/golunteer/backend/pkg/db" ) -type availabilitesDB struct { +type availabilitiesDB struct { Id int `db:"id"` Text string `db:"text"` Disabled bool `db:"disabled"` @@ -22,31 +22,31 @@ type Availability struct { var c *cache.Cache func Keys() (map[int]Availability, error) { - if availabilities, hit := c.Get("availabilites"); !hit { + if availabilities, hit := c.Get("availabilities"); !hit { refresh() - return nil, fmt.Errorf("availabilites not stored cached") + return nil, fmt.Errorf("availabilities not stored cached") } else { return availabilities.(map[int]Availability), nil } } func refresh() { - // get the availabilitesRaw from the database - var availabilitesRaw []availabilitesDB + // get the availabilitiesRaw from the database + var availabilitiesRaw []availabilitiesDB - if err := db.DB.Select(&availabilitesRaw, "SELECT * FROM AVAILABILITIES"); err == nil { + if err := db.DB.Select(&availabilitiesRaw, "SELECT * FROM AVAILABILITIES"); err == nil { // convert the result in a map - availabilites := map[int]Availability{} + availabilities := map[int]Availability{} - for _, a := range availabilitesRaw { - availabilites[a.Id] = Availability{ + for _, a := range availabilitiesRaw { + availabilities[a.Id] = Availability{ Text: a.Text, Disabled: a.Disabled, } } - c.Set("availabilites", availabilites) + c.Set("availabilities", availabilities) } } diff --git a/backend/pkg/db/availabilities/userAvailabilities.go b/backend/pkg/db/availabilities/userAvailabilities.go new file mode 100644 index 0000000..31abf0b --- /dev/null +++ b/backend/pkg/db/availabilities/userAvailabilities.go @@ -0,0 +1,36 @@ +package availabilities + +import ( + "github.com/johannesbuehl/golunteer/backend/pkg/db" + "github.com/johannesbuehl/golunteer/backend/pkg/db/users" +) + +type eventAvailabilities struct { + UserName string `db:"userName"` + AvailabilityID int `db:"availabilityID"` +} + +func Event(eventID int) (map[string]string, error) { + // get the availabilities for the event + var availabilitiesRows []eventAvailabilities + + if err := db.DB.Select(&availabilitiesRows, "SELECT userName, availabilityID FROM USER_AVAILABILITIES WHERE eventID = ?", eventID); err != nil { + return nil, err + } else { + // transform the result into a map + eventAvailabilities := map[string]string{} + + // get the availabilities + if availabilitiesMap, err := Keys(); err != nil { + return nil, err + } else if usersMap, err := users.Get(); err != nil { + return nil, err + } else { + for _, a := range availabilitiesRows { + eventAvailabilities[usersMap[a.UserName].Name] = availabilitiesMap[a.AvailabilityID].Text + } + + return eventAvailabilities, nil + } + } +} diff --git a/backend/pkg/db/events/events.go b/backend/pkg/db/events/events.go index 92d7d85..d9ab01c 100644 --- a/backend/pkg/db/events/events.go +++ b/backend/pkg/db/events/events.go @@ -3,6 +3,7 @@ package events import ( "github.com/johannesbuehl/golunteer/backend/pkg/db" "github.com/johannesbuehl/golunteer/backend/pkg/db/assignments" + "github.com/johannesbuehl/golunteer/backend/pkg/db/availabilities" "github.com/johannesbuehl/golunteer/backend/pkg/logger" ) @@ -11,6 +12,11 @@ type EventWithAssignment struct { Tasks map[string]*string `json:"tasks"` } +type EventWithAvailabilities struct { + EventWithAssignment + Availabilities map[string]string `json:"availabilities"` +} + type eventDataDB struct { Id int `db:"id" json:"id"` Date string `db:"date" json:"date" validate:"required"` @@ -18,18 +24,34 @@ type eventDataDB struct { } // transform the database-entry to an Event -func (e *eventDataDB) Event() (EventWithAssignment, error) { - // get the availabilites associated with the event +func (e eventDataDB) Event() (EventWithAssignment, error) { + // get the assignments associated with the event if assignemnts, err := assignments.Event(e.Id); err != nil { return EventWithAssignment{}, err } else { return EventWithAssignment{ - eventDataDB: *e, + eventDataDB: e, Tasks: assignemnts, }, nil } } +func (e eventDataDB) EventWithAvailabilities() (EventWithAvailabilities, error) { + // get the event with assignments + if event, err := e.Event(); err != nil { + return EventWithAvailabilities{}, err + + // get the availabilities + } else if availabilities, err := availabilities.Event(e.Id); err != nil { + return EventWithAvailabilities{}, err + } else { + return EventWithAvailabilities{ + EventWithAssignment: event, + Availabilities: availabilities, + }, nil + } +} + type EventCreate struct { eventDataDB Tasks []int `json:"tasks" validate:"required,min=1"` @@ -98,6 +120,25 @@ func WithAssignments() ([]EventWithAssignment, error) { } } +func WithAvailabilities() ([]EventWithAvailabilities, error) { + // get all events + if eventsDB, err := All(); err != nil { + return nil, err + } else { + events := make([]EventWithAvailabilities, len(eventsDB)) + + for ii, e := range eventsDB { + if ev, err := e.EventWithAvailabilities(); err != nil { + logger.Logger.Error().Msgf("can't get availabilities for event with id = %d: %v", e.Id, err) + } else { + events[ii] = ev + } + } + + return events, nil + } +} + func UserPending(userName string) (int, error) { var result struct { Count int `db:"count(*)"` diff --git a/backend/pkg/db/users/users.go b/backend/pkg/db/users/users.go index d7ad4e8..bac49ea 100644 --- a/backend/pkg/db/users/users.go +++ b/backend/pkg/db/users/users.go @@ -9,10 +9,10 @@ import ( ) type User struct { - Name string `db:"text"` + Name string `db:"name"` Password []byte `db:"password"` TokenID string `db:"tokenID"` - Admin bool `db:"disabled"` + Admin bool `db:"admin"` } var c *cache.Cache @@ -21,7 +21,7 @@ func Get() (map[string]User, error) { if users, hit := c.Get("users"); !hit { refresh() - return nil, fmt.Errorf("users not stored cached") + return nil, fmt.Errorf("users not cached") } else { return users.(map[string]User), nil } diff --git a/backend/pkg/router/events.go b/backend/pkg/router/events.go index 492133c..8bf63a6 100644 --- a/backend/pkg/router/events.go +++ b/backend/pkg/router/events.go @@ -56,6 +56,25 @@ func getEventsAssignments(args HandlerArgs) responseMessage { return response } +func getEventsAvailabilities(args HandlerArgs) responseMessage { + response := responseMessage{} + + // check for admin + if !args.User.Admin { + response.Status = fiber.StatusForbidden + } else { + if events, err := events.WithAvailabilities(); err != nil { + response.Status = fiber.StatusInternalServerError + + logger.Error().Msgf("can't retrieve events with availabilities: %v", err) + } else { + response.Data = events + } + } + + return response +} + func getEventsUserPending(args HandlerArgs) responseMessage { response := responseMessage{} diff --git a/backend/pkg/router/login.go b/backend/pkg/router/login.go index 10dd88d..ccadb1a 100644 --- a/backend/pkg/router/login.go +++ b/backend/pkg/router/login.go @@ -73,8 +73,8 @@ func handleLogin(c *fiber.Ctx) error { } else { // password is correct -> generate the JWT if jwt, err := config.SignJWT(JWTPayload{ - UserID: requestBody.Username, - TokenID: result.TokenID, + UserName: requestBody.Username, + TokenID: result.TokenID, }); err != nil { response.Status = fiber.StatusInternalServerError logger.Error().Msgf("can't create JWT: %v", err) diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go index 8521506..613aa3b 100644 --- a/backend/pkg/router/router.go +++ b/backend/pkg/router/router.go @@ -75,9 +75,10 @@ func init() { // map with the individual registered endpoints endpoints := map[string]map[string]func(HandlerArgs) responseMessage{ "GET": { - "events/assignments": getEventsAssignments, - "events/user/pending": getEventsUserPending, - "tasks": getTasks, + "events/assignments": getEventsAssignments, + "events/availabilities": getEventsAvailabilities, + "events/user/pending": getEventsUserPending, + "tasks": getTasks, }, "POST": {"events": postEvent}, "PATCH": {}, @@ -160,8 +161,8 @@ func removeSessionCookie(c *fiber.Ctx) { // payload of the JSON webtoken type JWTPayload struct { - UserID string `json:"userID"` - TokenID string `json:"tokenID"` + UserName string `json:"userName"` + TokenID string `json:"tokenID"` } // complete JSON webtoken @@ -172,7 +173,7 @@ type JWT struct { // extracts the json webtoken from the request // -// @returns (userID, tokenID, error) +// @returns (userName, tokenID, error) func extractJWT(c *fiber.Ctx) (string, string, error) { // get the session-cookie cookie := c.Cookies("session") @@ -191,7 +192,7 @@ func extractJWT(c *fiber.Ctx) (string, string, error) { // extract the claims from the JWT if claims, ok := token.Claims.(*JWT); ok && token.Valid { - return claims.CustomClaims.UserID, claims.CustomClaims.TokenID, nil + return claims.CustomClaims.UserName, claims.CustomClaims.TokenID, nil } else { return "", "", fmt.Errorf("invalid JWT") } diff --git a/client/src/Zustand.ts b/client/src/Zustand.ts index f489069..755d98d 100644 --- a/client/src/Zustand.ts +++ b/client/src/Zustand.ts @@ -3,6 +3,7 @@ import { DateFormatter as IntlDateFormatter } from "@internationalized/date"; import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { apiCall } from "./lib"; export type Task = string; @@ -24,7 +25,6 @@ interface Zustand { userName: string; admin: boolean; } | null; - tasks?: Record; setEvents: (events: EventData[]) => void; reset: (zustand?: Partial) => void; setPendingEvents: (c: number) => void; @@ -58,6 +58,23 @@ const zustand = create()( ), ); +export async function getTasks(): Promise< + Record +> { + const result = await apiCall<{ text: string; disabled: boolean }[]>( + "GET", + "tasks", + ); + + if (result.ok) { + const tasks = await result.json(); + + return tasks; + } else { + return []; + } +} + export class DateFormatter { private formatter; diff --git a/client/src/app/Overview.tsx b/client/src/app/Overview.tsx index 6d9a856..5ddf570 100644 --- a/client/src/app/Overview.tsx +++ b/client/src/app/Overview.tsx @@ -6,24 +6,11 @@ import { useState } from "react"; import AddEvent from "../components/Event/AddEvent"; import zustand from "../Zustand"; import AssignmentTable from "@/components/Event/AssignmentTable"; -import { useAsyncList } from "@react-stately/data"; -import { apiCall } from "@/lib"; import { Button } from "@nextui-org/react"; export default function EventVolunteer() { const [showAddItemDialogue, setShowAddItemDialogue] = useState(false); - // fetch the events from the server - useAsyncList({ - load: async () => { - const data = await apiCall("GET", "events"); - - return { - items: [], - }; - }, - }); - return (

Overview

diff --git a/client/src/app/admin/page.tsx b/client/src/app/admin/page.tsx index 428f017..17953fe 100644 --- a/client/src/app/admin/page.tsx +++ b/client/src/app/admin/page.tsx @@ -2,7 +2,8 @@ import AddEvent from "@/components/Event/AddEvent"; import LocalDate from "@/components/LocalDate"; -import zustand, { Availability, EventData, Task, Tasks } from "@/Zustand"; +import { apiCall } from "@/lib"; +import { Availability, EventData, getTasks, Task } from "@/Zustand"; import { Add, Copy, Edit, TrashCan } from "@carbon/icons-react"; import { Button, @@ -25,6 +26,8 @@ import { import { useAsyncList } from "@react-stately/data"; import React, { Key, useState } from "react"; +type EventWithAvailabilities = EventData & { availabilities: string[] }; + function availability2Tailwind(availability?: Availability) { switch (availability) { case "yes": @@ -46,19 +49,38 @@ function availability2Color(availability?: Availability) { } export default function AdminPanel() { - const tasks = [ - { key: "date", label: "Date" }, - { key: "description", label: "Description" }, - ...Tasks.map((task) => ({ label: task, key: task })), - { key: "actions", label: "Action" }, - ]; - - const list = useAsyncList({ + // get the available tasks and craft them into the headers + const headers = useAsyncList({ async load() { + const tasks = await getTasks(); + return { - items: [...zustand.getState().events], + items: [ + { key: "date", label: "Date" }, + { key: "description", label: "Description" }, + ...Object.entries(tasks) + .filter(([, task]) => !task.disabled) + .map(([id, task]) => ({ label: task.text, key: id })), + { key: "actions", label: "Action" }, + ], }; }, + }); + + // get the individual events + const events = useAsyncList({ + async load() { + const result = await apiCall( + "GET", + "events/availabilities", + ); + + if (result.ok) { + return { items: await result.json() }; + } else { + return { items: [] }; + } + }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { @@ -82,7 +104,10 @@ export default function AdminPanel() { }, }); - function getKeyValue(event: EventData, key: Key): React.ReactNode { + function getKeyValue( + event: EventWithAvailabilities, + key: Key, + ): React.ReactNode { switch (key) { case "date": return ( @@ -136,17 +161,17 @@ export default function AdminPanel() { }} className="[&_*]:overflow-visible" > - {Object.entries(event.volunteers).map( + {Object.entries(event.availabilities).map( ([volunteer, availability]) => ( - {volunteer} + {volunteer} ({availability}) ), )} @@ -157,7 +182,7 @@ export default function AdminPanel() { const [showAddEvent, setShowAddEvent] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [activeEvent, setActiveEvent] = useState(zustand.getState().events[0]); + const [activeEvent, setActiveEvent] = useState(); const topContent = (
@@ -181,8 +206,8 @@ export default function AdminPanel() { topContent={topContent} topContentPlacement="outside" isHeaderSticky - sortDescriptor={list.sortDescriptor} - onSortChange={list.sort} + sortDescriptor={events.sortDescriptor} + onSortChange={events.sort} classNames={{ wrapper: "bg-accent-4", tr: "even:bg-accent-5 ", @@ -191,7 +216,7 @@ export default function AdminPanel() { }} className="w-fit" > - + {(task) => ( )} - + {(event) => ( {(columnKey) => ( @@ -215,39 +240,41 @@ export default function AdminPanel() { - - - -

Confirm event deletion

-
- - The event{" "} - - - {activeEvent.date} - - {" "} - will be deleted. - - - - - -
-
+ {activeEvent !== undefined ? ( + + + +

Confirm event deletion

+
+ + The event{" "} + + + {activeEvent.date} + + {" "} + will be deleted. + + + + + +
+
+ ) : null}
); } diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 88d3537..c51013b 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -36,18 +36,8 @@ export default function RootLayout({ href: "/assignments", }, { - text: "Assign Tasks", - href: "/admin/assign", - admin: true, - }, - { - text: "Users", - href: "/admin/users", - admin: true, - }, - { - text: "Configuration", - href: "/admin/config", + text: "Admin", + href: "/admin", admin: true, }, ]; diff --git a/client/src/components/Event/AddEvent.tsx b/client/src/components/Event/AddEvent.tsx index 371d5e8..9486591 100644 --- a/client/src/components/Event/AddEvent.tsx +++ b/client/src/components/Event/AddEvent.tsx @@ -1,6 +1,6 @@ import { useEffect, useReducer } from "react"; import { Add } from "@carbon/icons-react"; -import zustand, { Task } from "../../Zustand"; +import zustand, { getTasks, Task } from "../../Zustand"; import { getLocalTimeZone, now, ZonedDateTime } from "@internationalized/date"; import { Button, @@ -54,20 +54,7 @@ export default function AddEvent(props: { // get the available tasks useEffect(() => { - (async () => { - const result = await apiCall<{ text: string; disabled: boolean }[]>( - "GET", - "tasks", - ); - - if (result.ok) { - const tasks = await result.json(); - - zustand.setState(() => ({ - tasks, - })); - } - })(); + void getTasks(); }, []); // sends the addEvent request to the backend