implemented assigning users to events
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user