implemented assigning users to events

This commit is contained in:
z1glr
2025-01-23 21:29:08 +00:00
parent 19e7d2b366
commit 00ab161261
8 changed files with 229 additions and 42 deletions

View File

@@ -9,7 +9,9 @@ type eventAvailabilities struct {
AvailabilityID int `db:"availabilityID"` AvailabilityID int `db:"availabilityID"`
} }
func Event(eventID int) (map[string]string, error) { type AvailabilityMap map[int][]string
func Event(eventID int) (AvailabilityMap, error) {
// get the availabilities for the event // get the availabilities for the event
var availabilitiesRows []eventAvailabilities var availabilitiesRows []eventAvailabilities
@@ -17,17 +19,18 @@ func Event(eventID int) (map[string]string, error) {
return nil, err return nil, err
} else { } else {
// transform the result into a map // transform the result into a map
eventAvailabilities := map[string]string{} eventAvailabilities := AvailabilityMap{}
// get the availabilities // get the availabilities
if availabilitiesMap, err := Keys(); err != nil {
return nil, err
} else {
for _, a := range availabilitiesRows { for _, a := range availabilitiesRows {
eventAvailabilities[a.UserName] = availabilitiesMap[a.AvailabilityID].AvailabilityName // if there is no slice for this availability, create it
if _, exists := eventAvailabilities[a.AvailabilityID]; !exists {
eventAvailabilities[a.AvailabilityID] = make([]string, 0)
}
eventAvailabilities[a.AvailabilityID] = append(eventAvailabilities[a.AvailabilityID], a.UserName)
} }
return eventAvailabilities, nil return eventAvailabilities, nil
} }
} }
}

View File

@@ -32,7 +32,7 @@ type EventWithAssignment struct {
type EventWithAvailabilities struct { type EventWithAvailabilities struct {
EventWithAssignment EventWithAssignment
Availabilities map[string]string `json:"availabilities"` Availabilities availabilities.AvailabilityMap `json:"availabilities"`
} }
type EventCreate struct { type EventCreate struct {
@@ -268,8 +268,22 @@ func User(userName string) ([]EventWithAssignment, error) {
} }
// set the availability of an user for a specific event // set the availability of an user for a specific event
func UserAvailability(userName string, eventID, availabilityID int) error { func UserAvailability(eventID, availabilityID int, userName string) error {
_, err := db.DB.Exec("INSERT INTO USER_AVAILABILITIES (userName, eventID, availabilityID) VALUES ($1, $2, $3) ON CONFLICT (userName, eventID) DO UPDATE SET availabilityID = $3", userName, eventID, availabilityID) _, err := db.DB.Exec("INSERT INTO USER_AVAILABILITIES (userName, eventID, availabilityID) VALUES ($1, $2, $3) ON CONFLICT (userName, eventID) DO UPDATE SET availabilityID = $3", userName, eventID, availabilityID)
return err return err
} }
// set the assignment of an user to a task for a specific event
func SetAssignment(eventID, taskID int, userName string) error {
_, err := db.DB.Exec("UPDATE USER_ASSIGNMENTS SET userName = $1 WHERE eventID = $2 AND taskID = $3", userName, eventID, taskID)
return err
}
// remove the assignment of an user
func DeleteAssignment(eventID, taskID int) error {
_, err := db.DB.Exec("UPDATE USER_ASSIGNMENTS SET userName = null WHERE eventID = $1 AND taskID = $2", eventID, taskID)
return err
}

View File

@@ -137,14 +137,74 @@ func (a *Handler) putEventUserAvailability() {
logger.Log().Msgf("setting user-event-availability failed: can't get parse: %v", err) logger.Log().Msgf("setting user-event-availability failed: can't get parse: %v", err)
// insert the availability into the database // insert the availability into the database
} else if err := events.UserAvailability(a.UserName, eventID, availabilityID); err != nil { } else if err := events.UserAvailability(eventID, availabilityID, a.UserName); err != nil {
a.Status = fiber.StatusInternalServerError a.Status = fiber.StatusInternalServerError
logger.Error().Msgf("setting user-event-availability failed: can't write availability to database: %v", err) logger.Error().Msgf("setting user-event-availability failed: can't write availability to database: %v", err)
} }
} }
}
func (a *Handler) putEventAssignment() {
// check admin
if !a.Admin {
a.Status = fiber.StatusUnauthorized
logger.Warn().Msg("setting event-assignment failed: user is no admin")
// retrieve the eventID from the query
} else if eventID := a.C.QueryInt("eventID", -1); eventID == -1 {
a.Status = fiber.StatusBadRequest
logger.Warn().Msg("setting event-assignment failed: query is missing \"eventID\"")
// retrieve the taskID from the query
} else if taskID := a.C.QueryInt("taskID", -1); taskID == -1 {
a.Status = fiber.StatusBadRequest
logger.Warn().Msg("setting event-assignment failed: query is missing \"taskID\"")
// parse the body // parse the body
} else if userName := string(a.C.Body()); userName == "" {
a.Status = fiber.StatusBadRequest
logger.Warn().Msg("setting event-assignment failed: body is missing")
// set the availability in the database
} else if err := events.SetAssignment(eventID, taskID, userName); err != nil {
a.Status = fiber.StatusBadRequest
logger.Warn().Msgf("setting event-assignment failed: can't write to database: %v", err)
}
}
func (a *Handler) deleteEventAssignment() {
// check admin
if !a.Admin {
a.Status = fiber.StatusUnauthorized
logger.Warn().Msg("deleting event-assignment failed: user is no admin")
// retrieve the eventID from the query
} else if eventID := a.C.QueryInt("eventID", -1); eventID == -1 {
a.Status = fiber.StatusBadRequest
logger.Warn().Msg("deleting event-assignment failed: query is missing \"eventID\"")
// retrieve the taskID from the query
} else if taskID := a.C.QueryInt("taskID", -1); taskID == -1 {
a.Status = fiber.StatusBadRequest
logger.Warn().Msg("deleting event-assignment failed: query is missing \"taskID\"")
// set the availability in the database
} else if err := events.DeleteAssignment(eventID, taskID); err != nil {
a.Status = fiber.StatusBadRequest
logger.Warn().Msgf("deleting event-assignment failed: can't write to database: %v", err)
}
} }
func (a *Handler) deleteEvent() { func (a *Handler) deleteEvent() {

View File

@@ -107,11 +107,13 @@ func init() {
"tasks": (*Handler).patchTask, // modify a task "tasks": (*Handler).patchTask, // modify a task
}, },
"PUT": { "PUT": {
"users/password": (*Handler).putPassword, // change the password
"events/user/availability": (*Handler).putEventUserAvailability, // set or change the users availability for a specific event "events/user/availability": (*Handler).putEventUserAvailability, // set or change the users availability for a specific event
"events/assignments": (*Handler).putEventAssignment,
"users/password": (*Handler).putPassword, // change the password
}, },
"DELETE": { "DELETE": {
"event": (*Handler).deleteEvent, // remove an event "event": (*Handler).deleteEvent, // remove an event
"events/assignments": (*Handler).deleteEventAssignment,
"tasks": (*Handler).deleteTask, // remove a task "tasks": (*Handler).deleteTask, // remove a task
"availabilities": (*Handler).deleteAvailability, // remove an availability "availabilities": (*Handler).deleteAvailability, // remove an availability
"users": (*Handler).deleteUser, // remove an user "users": (*Handler).deleteUser, // remove an user

View File

@@ -10,8 +10,12 @@ export default function MyEvents() {
const result = await apiCall<EventData[]>("GET", "events/user/assigned"); const result = await apiCall<EventData[]>("GET", "events/user/assigned");
if (result.ok) { if (result.ok) {
const data = await result.json();
console.debug(data);
return { return {
items: await result.json(), items: data,
}; };
} else { } else {
return { return {

View File

@@ -3,7 +3,7 @@
import AddEvent from "@/components/Event/AddEvent"; import AddEvent from "@/components/Event/AddEvent";
import EditEvent from "@/components/Event/EditEvent"; import EditEvent from "@/components/Event/EditEvent";
import LocalDate from "@/components/LocalDate"; import LocalDate from "@/components/LocalDate";
import { apiCall, getTasks } from "@/lib"; import { apiCall, getAvailabilities, getTasks } from "@/lib";
import { EventData } from "@/Zustand"; import { EventData } from "@/Zustand";
import { import {
Add, Add,
@@ -21,6 +21,7 @@ import {
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownSection,
DropdownTrigger, DropdownTrigger,
Modal, Modal,
ModalBody, ModalBody,
@@ -37,14 +38,21 @@ import {
Tooltip, Tooltip,
} from "@heroui/react"; } from "@heroui/react";
import { useAsyncList } from "@react-stately/data"; import { useAsyncList } from "@react-stately/data";
import React, { Key, useState } from "react"; import React, { Key, ReactElement, useEffect, useState } from "react";
import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
import AvailabilityChip from "@/components/AvailabilityChip";
type EventWithAvailabilities = EventData & { availabilities: string[] }; type EventWithAvailabilities = EventData & {
availabilities: Record<string, string[]>;
};
export default function AdminPanel() { export default function AdminPanel() {
const [showAddEvent, setShowAddEvent] = useState(false); const [showAddEvent, setShowAddEvent] = useState(false);
const [editEvent, setEditEvent] = useState<EventData | undefined>(); const [editEvent, setEditEvent] = useState<EventData | undefined>();
const [deleteEvent, setDeleteEvent] = useState<EventData | undefined>(); const [deleteEvent, setDeleteEvent] = useState<EventData | undefined>();
const [availabilityMap, setAvailabilityMap] = useState<
Record<number, Availability>
>({});
// get the available tasks and craft them into the headers // get the available tasks and craft them into the headers
const headers = useAsyncList<{ const headers = useAsyncList<{
@@ -113,6 +121,51 @@ export default function AdminPanel() {
}, },
}); });
// retrieve the availabilites and store them in a map
useEffect(() => {
(async () => {
setAvailabilityMap(
Object.fromEntries(
(await getAvailabilities()).map((a) => [a.availabilityID, a]),
),
);
})();
}, []);
function getAvailabilityById(availabilityID: number): Availability {
return availabilityMap[availabilityID];
}
// send a command to the backend to assign a volunteer to a task
async function sendVolunteerAssignment(
eventID: number,
taskID: number,
userName: string,
) {
const result = await apiCall(
"PUT",
"events/assignments",
{ eventID, taskID },
userName,
);
if (result.ok) {
events.reload();
}
}
// sends a command to the backend to remove an volunteer-assignment
async function removeVolunteerAssignment(eventID: number, taskID: number) {
const result = await apiCall("DELETE", "events/assignments", {
eventID,
taskID,
});
if (result.ok) {
events.reload();
}
}
// send a delete request to the backend and close the popup on success // send a delete request to the backend and close the popup on success
async function sendDeleteEvent() { async function sendDeleteEvent() {
if (deleteEvent !== undefined) { if (deleteEvent !== undefined) {
@@ -123,6 +176,9 @@ export default function AdminPanel() {
if (result.ok) { if (result.ok) {
// store the received events // store the received events
events.reload(); events.reload();
// close the delete-confirmaton
setDeleteEvent(undefined);
} }
} }
} }
@@ -184,9 +240,13 @@ export default function AdminPanel() {
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
{!!event.tasks.find((t) => t.taskID === key)?.userName ? ( {!!event.tasks.find((t) => t.taskID == key)?.userName ? (
<Chip onClose={() => alert("implement")}> <Chip
{event.tasks.find((t) => t.taskID === key)?.taskName} onClose={() =>
removeVolunteerAssignment(event.eventID, key as number)
}
>
{event.tasks.find((t) => t.taskID == key)?.userName}
</Chip> </Chip>
) : ( ) : (
<Button isIconOnly size="sm" radius="md" variant="flat"> <Button isIconOnly size="sm" radius="md" variant="flat">
@@ -194,19 +254,50 @@ export default function AdminPanel() {
</Button> </Button>
)} )}
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu> <DropdownMenu
{Object.entries(event.availabilities).map( onAction={(a) =>
([volunteer, availability]) => ( sendVolunteerAssignment(
<DropdownItem event.eventID,
key={volunteer} key as number,
// color={availability2Color(availability)} a as string,
className={[ )
// "text-" + availability2Color(availability), }
// availability2Tailwind(availability),
].join(" ")}
> >
{volunteer} ({availability}) {Object.entries(event.availabilities).map(
(
[availabilityId, volunteers],
iAvailability,
aAvailabilities,
) => (
<DropdownSection
key={availabilityId}
showDivider={iAvailability < aAvailabilities.length - 1}
classNames={{
base: "flex flex-col justify-start",
heading: "mx-auto",
}}
className="justi"
title={
(
<AvailabilityChip
availability={getAvailabilityById(
parseInt(availabilityId),
)}
/>
) as ReactElement & string
}
>
{volunteers.map((v) => (
<DropdownItem
key={v}
classNames={{
base: "", // this empty class is needed, else some styles are applied
}}
>
{v}
</DropdownItem> </DropdownItem>
))}
</DropdownSection>
), ),
)} )}
</DropdownMenu> </DropdownMenu>

View File

@@ -6,11 +6,11 @@ export default function AvailabilityChip({
availability, availability,
className, className,
}: { }: {
availability: Availability; availability?: Availability;
className?: string; className?: string;
classNames?: ChipProps["classNames"]; classNames?: ChipProps["classNames"];
}) { }) {
return ( return !!availability ? (
<Chip <Chip
classNames={{ classNames={{
base: `bg-${color2Tailwind(availability.color)}`, base: `bg-${color2Tailwind(availability.color)}`,
@@ -19,5 +19,5 @@ export default function AvailabilityChip({
> >
{availability.availabilityName} {availability.availabilityName}
</Chip> </Chip>
); ) : null;
} }

View File

@@ -59,7 +59,7 @@ export async function apiCall<K>(
}, },
credentials: "include", credentials: "include",
method, method,
body: body !== undefined ? JSON.stringify(body) : undefined, body: prepareBody(body),
}); });
return response; return response;
@@ -79,6 +79,19 @@ function getContentType(type: string): string {
} }
} }
function prepareBody(
body: object | number | string | boolean | undefined,
): BodyInit | undefined {
switch (typeof body) {
case "object":
return JSON.stringify(body);
case "undefined":
return undefined;
default:
return body.toString();
}
}
export function classNames(classNames: Record<string, boolean>): string { export function classNames(classNames: Record<string, boolean>): string {
return Object.entries(classNames) return Object.entries(classNames)
.map(([classString, value]) => { .map(([classString, value]) => {
@@ -157,11 +170,11 @@ export async function getAvailabilities(): Promise<Availability[]> {
const result = await apiCall<Availability[]>("GET", "availabilities"); const result = await apiCall<Availability[]>("GET", "availabilities");
if (result.ok) { if (result.ok) {
const tasks = await result.json(); const availabilities = await result.json();
state.patch({ availabilities: tasks }); state.patch({ availabilities: availabilities });
return tasks; return availabilities;
} else { } else {
return []; return [];
} }