added my assigned events overview

This commit is contained in:
z1glr
2025-01-24 00:27:53 +00:00
parent 00ab161261
commit becaaa35bc
7 changed files with 197 additions and 134 deletions

View File

@@ -25,13 +25,13 @@ type EventAssignment struct {
UserName *string `db:"userName" json:"userName"` UserName *string `db:"userName" json:"userName"`
} }
type EventWithAssignment struct { type EventWithAssignments struct {
EventData EventData
Tasks []EventAssignment `json:"tasks"` Tasks []EventAssignment `json:"tasks"`
} }
type EventWithAvailabilities struct { type EventWithAvailabilities struct {
EventWithAssignment EventWithAssignments
Availabilities availabilities.AvailabilityMap `json:"availabilities"` Availabilities availabilities.AvailabilityMap `json:"availabilities"`
} }
@@ -41,22 +41,22 @@ type EventCreate struct {
Tasks []int `json:"tasks" validate:"required,min=1"` Tasks []int `json:"tasks" validate:"required,min=1"`
} }
// transform the database-entry to an Event // transform the database-entry to an WithAssignments
func (e EventData) Event() (EventWithAssignment, error) { func (e EventData) WithAssignments() (EventWithAssignments, error) {
// get the assignments associated with the event // get the assignments associated with the event
if assignemnts, err := Assignments(e.EventID); err != nil { if assignemnts, err := Assignments(e.EventID); err != nil {
return EventWithAssignment{}, err return EventWithAssignments{}, err
} else { } else {
return EventWithAssignment{ return EventWithAssignments{
EventData: e, EventData: e,
Tasks: assignemnts, Tasks: assignemnts,
}, nil }, nil
} }
} }
func (e EventData) EventWithAvailabilities() (EventWithAvailabilities, error) { func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) {
// get the event with assignments // get the event with assignments
if event, err := e.Event(); err != nil { if event, err := e.WithAssignments(); err != nil {
return EventWithAvailabilities{}, err return EventWithAvailabilities{}, err
// get the availabilities // get the availabilities
@@ -64,7 +64,7 @@ func (e EventData) EventWithAvailabilities() (EventWithAvailabilities, error) {
return EventWithAvailabilities{}, err return EventWithAvailabilities{}, err
} else { } else {
return EventWithAvailabilities{ return EventWithAvailabilities{
EventWithAssignment: event, EventWithAssignments: event,
Availabilities: availabilities, Availabilities: availabilities,
}, nil }, nil
} }
@@ -173,15 +173,15 @@ func All() ([]EventData, error) {
} }
} }
func WithAssignments() ([]EventWithAssignment, error) { func WithAssignments() ([]EventWithAssignments, error) {
// get all events // get all events
if eventsDB, err := All(); err != nil { if eventsDB, err := All(); err != nil {
return nil, err return nil, err
} else { } else {
events := make([]EventWithAssignment, len(eventsDB)) events := make([]EventWithAssignments, len(eventsDB))
for ii, e := range eventsDB { for ii, e := range eventsDB {
if ev, err := e.Event(); err != nil { if ev, err := e.WithAssignments(); err != nil {
logger.Logger.Error().Msgf("can't get assignments for event with assignmentID = %d: %v", e.EventID, err) logger.Logger.Error().Msgf("can't get assignments for event with assignmentID = %d: %v", e.EventID, err)
} else { } else {
events[ii] = ev events[ii] = ev
@@ -200,8 +200,14 @@ func WithAvailabilities() ([]EventWithAvailabilities, error) {
events := make([]EventWithAvailabilities, len(eventsDB)) events := make([]EventWithAvailabilities, len(eventsDB))
for ii, e := range eventsDB { for ii, e := range eventsDB {
if ev, err := e.EventWithAvailabilities(); err != nil { if ev, err := e.WithAvailabilities(); err != nil {
logger.Logger.Error().Msgf("can't get availabilities for event with eventID = %d: %v", e.EventID, err) logger.Logger.Error().Msgf("can't get availabilities for event with eventID = %d: %v", e.EventID, err)
// remove the last element from the return-slice, since there is now one element less
if len(events) > 0 {
events = events[:len(events)-1]
}
} else { } else {
events[ii] = ev events[ii] = ev
} }
@@ -250,20 +256,33 @@ func Assignments(eventID int) ([]EventAssignment, error) {
} }
} }
func User(userName string) ([]EventWithAssignment, error) { func User(userName string) ([]EventWithAssignments, error) {
// get all assignments of the user // get all assignments of the user
// var events []EventWithAssignment // var eventsDB []EventWithAssignment
var events []struct { var eventsDB []EventData
EventData
TaskID int `db:"taskID"`
UserName string `db:"userName"`
}
if err := db.DB.Select(&events, "SELECT DISTINCT * FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil { // get all the events where the volunteer is assigned a task
if err := db.DB.Select(&eventsDB, "SELECT DISTINCT EVENTS.date, EVENTS.description, EVENTS.eventID FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil {
return nil, err return nil, err
} else { } else {
return nil, nil // for each event create an event with assignments
events := make([]EventWithAssignments, len(eventsDB))
for ii, event := range eventsDB {
if eventsWithAssignment, err := event.WithAssignments(); err != nil {
logger.Logger.Error().Msgf("can't get assignments for event with eventID = %d: %v", event.EventID, err)
// remove the last element from the return-slice, since there is now one element less
if len(events) > 0 {
events = events[:len(events)-1]
}
} else {
events[ii] = eventsWithAssignment
}
}
return events, nil
} }
} }

View File

@@ -14,7 +14,7 @@ export type EventData = BaseEvent & {
tasks: TaskAssignment[]; tasks: TaskAssignment[];
}; };
interface TaskAssignment { export interface TaskAssignment {
taskID: number; taskID: number;
taskName: string; taskName: string;
userName: string | null; userName: string | null;

View File

@@ -1,10 +1,14 @@
"use client"; "use client";
import AssignmentTable from "@/components/Event/AssignmentTable";
import Event from "@/components/Event/Event";
import { apiCall } from "@/lib"; import { apiCall } from "@/lib";
import { EventData } from "@/Zustand"; import zustand, { EventData } from "@/Zustand";
import { useAsyncList } from "@react-stately/data"; import { useAsyncList } from "@react-stately/data";
export default function MyEvents() { export default function MyEvents() {
const user = zustand((state) => state.user);
const events = useAsyncList({ const events = useAsyncList({
async load() { async load() {
const result = await apiCall<EventData[]>("GET", "events/user/assigned"); const result = await apiCall<EventData[]>("GET", "events/user/assigned");
@@ -26,8 +30,12 @@ export default function MyEvents() {
}); });
return ( return (
<div> <div className="flex justify-center gap-4">
<h2>{events.items.map((e) => e.date)}</h2> {events.items.map((e) => (
<Event key={e.eventID} event={e}>
<AssignmentTable tasks={e.tasks} highlightUser={user?.userName} />
</Event>
))}
</div> </div>
); );
} }

View File

@@ -8,8 +8,8 @@ export default function Overview() {
<h1 className="mb-4 text-center text-4xl">My Events</h1> <h1 className="mb-4 text-center text-4xl">My Events</h1>
<MyEvents /> <MyEvents />
<h1 className="mb-4 text-center text-4xl"> <h1 className="mb-4 mt-8 text-center text-4xl">
events that I don't have entered an availability yet events that I don&apos;t have entered an availability yet
</h1> </h1>
<PengingEvents /> <PengingEvents />
</div> </div>

View File

@@ -0,0 +1,116 @@
import AvailabilityChip from "@/components/AvailabilityChip";
import { TaskAssignment } from "@/Zustand";
import { AddLarge } from "@carbon/icons-react";
import {
Button,
Chip,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownSection,
DropdownTrigger,
} from "@heroui/react";
import { EventWithAvailabilities } from "./page";
import { ReactElement } from "react";
import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
import { apiCall, classNames } from "@/lib";
export default function VolunteerSelector({
event,
task,
getAvailabilityById,
onReloadRequest,
}: {
event: EventWithAvailabilities;
task: TaskAssignment;
getAvailabilityById: (availabilityID: number) => Availability;
onReloadRequest: () => void;
}) {
async function sendVolunteerAssignment(
eventID: number,
taskID: number,
userName: string,
) {
const result = await apiCall(
"PUT",
"events/assignments",
{ eventID, taskID },
userName,
);
if (result.ok) {
onReloadRequest();
}
}
// 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) {
onReloadRequest();
}
}
return (
<Dropdown>
<DropdownTrigger>
{!!event.tasks.find((t) => t.taskID == task.taskID)?.userName ? (
<Chip
onClose={() =>
removeVolunteerAssignment(event.eventID, task.taskID)
}
>
{event.tasks.find((t) => t.taskID == task.taskID)?.userName}
</Chip>
) : (
<Button isIconOnly size="sm" radius="md" variant="flat">
<AddLarge className="mx-auto" />
</Button>
)}
</DropdownTrigger>
<DropdownMenu
onAction={(a) =>
sendVolunteerAssignment(event.eventID, task.taskID, a as string)
}
>
{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",
}}
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
title: classNames({
"text-primary font-bold": v === task.userName,
}),
}}
>
{v}
</DropdownItem>
))}
</DropdownSection>
),
)}
</DropdownMenu>
</Dropdown>
);
}

View File

@@ -7,7 +7,6 @@ import { apiCall, getAvailabilities, getTasks } from "@/lib";
import { EventData } from "@/Zustand"; import { EventData } from "@/Zustand";
import { import {
Add, Add,
AddLarge,
Copy, Copy,
Edit, Edit,
NotAvailable, NotAvailable,
@@ -17,12 +16,6 @@ import {
import { import {
Button, Button,
ButtonGroup, ButtonGroup,
Chip,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownSection,
DropdownTrigger,
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
@@ -38,11 +31,11 @@ import {
Tooltip, Tooltip,
} from "@heroui/react"; } from "@heroui/react";
import { useAsyncList } from "@react-stately/data"; import { useAsyncList } from "@react-stately/data";
import React, { Key, ReactElement, useEffect, useState } from "react"; import React, { Key, useEffect, useState } from "react";
import { Availability } from "../admin/(availabilities)/AvailabilityEditor"; import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
import AvailabilityChip from "@/components/AvailabilityChip"; import VolunteerSelector from "./VolunteerSelector";
type EventWithAvailabilities = EventData & { export type EventWithAvailabilities = EventData & {
availabilities: Record<string, string[]>; availabilities: Record<string, string[]>;
}; };
@@ -137,35 +130,6 @@ export default function AdminPanel() {
} }
// send a command to the backend to assign a volunteer to a task // 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) {
@@ -236,72 +200,16 @@ export default function AdminPanel() {
); );
default: default:
// only show the selector, if the task is needed for the event // only show the selector, if the task is needed for the event
if (event.tasks?.some((t) => t.taskID == key)) { const task = event.tasks.find((t) => t.taskID == key);
if (!!task) {
return ( return (
<Dropdown> <VolunteerSelector
<DropdownTrigger> event={event}
{!!event.tasks.find((t) => t.taskID == key)?.userName ? ( task={task}
<Chip getAvailabilityById={getAvailabilityById}
onClose={() => onReloadRequest={events.reload}
removeVolunteerAssignment(event.eventID, key as number)
}
>
{event.tasks.find((t) => t.taskID == key)?.userName}
</Chip>
) : (
<Button isIconOnly size="sm" radius="md" variant="flat">
<AddLarge className="mx-auto" />
</Button>
)}
</DropdownTrigger>
<DropdownMenu
onAction={(a) =>
sendVolunteerAssignment(
event.eventID,
key as number,
a as string,
)
}
>
{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>
))}
</DropdownSection>
),
)}
</DropdownMenu>
</Dropdown>
); );
} else { } else {
return <NotAvailable className="mx-auto text-foreground-300" />; return <NotAvailable className="mx-auto text-foreground-300" />;

View File

@@ -1,15 +1,27 @@
import { classNames } from "@/lib";
import { EventData } from "@/Zustand"; import { EventData } from "@/Zustand";
export default function AssignmentTable({ export default function AssignmentTable({
tasks, tasks,
highlightTask,
highlightUser,
}: { }: {
tasks: EventData["tasks"]; tasks: EventData["tasks"];
highlightUser?: string;
highlightTask?: string;
}) { }) {
return ( return (
<table> <table>
<tbody> <tbody>
{tasks.map((task) => ( {tasks.map((task) => (
<tr key={task.taskID}> <tr
key={task.taskID}
className={classNames({
"text-danger":
task.userName === highlightUser ||
task.taskName === highlightTask,
})}
>
<th className="pr-4 text-left">{task.taskName}</th> <th className="pr-4 text-left">{task.taskName}</th>
<td> <td>
{task.userName ?? ( {task.userName ?? (