added viewing all availabilities in events view

This commit is contained in:
z1glr
2025-03-13 08:55:39 +00:00
parent 650ed86504
commit 9941b1526c
20 changed files with 2325 additions and 2290 deletions

View File

@@ -2,4 +2,5 @@
- readme text - readme text
- check enter pressing on modals - check enter pressing on modals
- add control-enter on event-description - add availability table
- add availability notes

View File

@@ -48,7 +48,7 @@ type EventWithAvailabilities struct {
} }
type EventWithAssignmentsUserAvailability struct { type EventWithAssignmentsUserAvailability struct {
EventWithAssignments EventWithAvailabilities
Availability *int `json:"availability" db:"availabilityID"` Availability *int `json:"availability" db:"availabilityID"`
} }
@@ -71,10 +71,10 @@ func (e EventData) WithAssignments() (EventWithAssignments, error) {
} }
} }
func (e EventWithAssignments) WithUserAvailability(userName string) (EventWithAssignmentsUserAvailability, error) { func (e EventWithAvailabilities) WithUserAvailability(userName string) (EventWithAssignmentsUserAvailability, error) {
// get the availability of the user // get the availability of the user
event := EventWithAssignmentsUserAvailability{ event := EventWithAssignmentsUserAvailability{
EventWithAssignments: e, EventWithAvailabilities: e,
} }
if err := db.DB.Select(&event, "SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", e.EventID, userName); err != nil { if err := db.DB.Select(&event, "SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", e.EventID, userName); err != nil {

View File

@@ -130,11 +130,11 @@ func (userName UserName) WithUserAvailability() ([]events.EventWithAssignmentsUs
} else { } else {
// get the assignments for every event // get the assignments for every event
for ii, event := range events { for ii, event := range events {
if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAssignments(); err != nil { if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAvailabilities(); err != nil {
// remove the current event from the events // remove the current event from the events
events = append(events[:ii], events[ii+1:]...) events = append(events[:ii], events[ii+1:]...)
} else { } else {
events[ii].EventWithAssignments = eventWithAssignments events[ii].EventWithAvailabilities = eventWithAssignments
} }
} }

View File

@@ -80,10 +80,6 @@ func (a *Handler) getEventsAssignments() {
} }
func (a *Handler) getEventsAvailabilities() { func (a *Handler) getEventsAvailabilities() {
// check for admin
if !a.Admin {
a.Status = fiber.StatusForbidden
} else {
if events, err := events.WithAvailabilities(); err != nil { if events, err := events.WithAvailabilities(); err != nil {
a.Status = fiber.StatusInternalServerError a.Status = fiber.StatusInternalServerError
@@ -91,7 +87,6 @@ func (a *Handler) getEventsAvailabilities() {
} else { } else {
a.Data = events a.Data = events
} }
}
} }
func (a *Handler) getEventUserAssignmentAvailability() { func (a *Handler) getEventUserAssignmentAvailability() {

4288
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@carbon/icons-react": "^11.53.0", "@carbon/icons-react": "^11.53.0",
"@heroui/react": "^2.6.14", "@heroui/react": "^2.6.14",
"@internationalized/date": "3.6.0", "@internationalized/date": "3.7.0",
"@mantine/hooks": "^7.16.2", "@mantine/hooks": "^7.16.2",
"@react-aria/i18n": "^3.12.4", "@react-aria/i18n": "^3.12.4",
"@react-stately/data": "^3.12.0", "@react-stately/data": "^3.12.0",

View File

@@ -14,11 +14,17 @@ export type EventAvailability = BaseEvent & {
availability: number; availability: number;
}; };
export type EventAvailabilities = BaseEvent & {
availabilities: Record<number, string[]>;
};
export type EventData = BaseEvent & { export type EventData = BaseEvent & {
tasks: TaskAssignment[]; tasks: TaskAssignment[];
}; };
export type EventDataWithAvailability = EventData & EventAvailability; export type EventDataWithAvailabilityAvailabilities = EventData &
EventAvailability &
EventAvailabilities;
export interface TaskAssignment { export interface TaskAssignment {
taskID: number; taskID: number;

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import AssignmentTable from "@/components/Event/AssignmentTable"; import AssignmentGrid from "@/components/Event/AssignmentTable";
import Event from "@/components/Event/Event"; import Event from "@/components/Event/Event";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import { apiCall } from "@/lib"; import { apiCall } from "@/lib";
@@ -37,7 +37,7 @@ export default function MyEvents() {
<div className="flex flex-wrap justify-center gap-4"> <div className="flex flex-wrap justify-center gap-4">
{events.items.map((e) => ( {events.items.map((e) => (
<Event key={e.eventID} event={e}> <Event key={e.eventID} event={e}>
<AssignmentTable <AssignmentGrid
className="mt-auto" className="mt-auto"
tasks={e.tasks} tasks={e.tasks}
highlightUser={user?.userName} highlightUser={user?.userName}

View File

@@ -137,7 +137,7 @@ export default function Availabilities() {
{(availability) => ( {(availability) => (
<TableRow key={availability.availabilityName}> <TableRow key={availability.availabilityName}>
<TableCell> <TableCell>
<AvailabilityChip availability={availability} /> <AvailabilityChip>{availability}</AvailabilityChip>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Checkbox isSelected={availability.enabled} /> <Checkbox isSelected={availability.enabled} />
@@ -199,7 +199,7 @@ export default function Availabilities() {
{!!deleteAvailability ? ( {!!deleteAvailability ? (
<> <>
The availability{" "} The availability{" "}
<AvailabilityChip availability={deleteAvailability} /> will be <AvailabilityChip>{deleteAvailability}</AvailabilityChip>
deleted. deleted.
</> </>
) : null} ) : null}

View File

@@ -26,7 +26,7 @@ export default function EditAvailability(props: {
<> <>
Edit Availability{" "} Edit Availability{" "}
{!!props.value ? ( {!!props.value ? (
<AvailabilityChip availability={props.value} className="ms-4" /> <AvailabilityChip className="ms-4">{props.value}</AvailabilityChip>
) : null} ) : null}
</> </>
} }

View File

@@ -129,9 +129,9 @@ export default function VolunteerSelector({
}} }}
title={ title={
( (
<AvailabilityChip <AvailabilityChip>
availability={getAvailabilityById(parseInt(availabilityId))} {getAvailabilityById(parseInt(availabilityId))}
/> </AvailabilityChip>
) as ReactElement & string ) as ReactElement & string
} }
> >

View File

@@ -1,24 +1,50 @@
"use client"; "use client";
import AddEvent from "@/components/Event/AddEvent"; import AddEvent from "@/components/Event/AddEvent";
import AssignmentTable from "@/components/Event/AssignmentTable"; import AssigmentTable from "@/components/Event/AssignmentTable";
import Event from "@/components/Event/Event"; import Event from "@/components/Event/Event";
import { apiCall, getUserTasks } from "@/lib"; import { apiCall, getAvailabilities, getUserTasks } from "@/lib";
import zustand, { EventDataWithAvailability } from "@/Zustand"; import zustand, { EventDataWithAvailabilityAvailabilities } from "@/Zustand";
import { Add } from "@carbon/icons-react"; import { Add, Filter } from "@carbon/icons-react";
import { Button, Tab, Tabs } from "@heroui/react"; import {
Button,
Checkbox,
CheckboxGroup,
Popover,
PopoverContent,
PopoverTrigger,
Tab,
Tabs,
} from "@heroui/react";
import { useAsyncList } from "@react-stately/data"; import { useAsyncList } from "@react-stately/data";
import { useState } from "react"; import { useState } from "react";
import AvailabilitySelector from "@/components/Event/AvailabilitySelector"; import AvailabilitySelector from "@/components/Event/AvailabilitySelector";
import AvailabilityTable from "@/components/Event/AvailabilityTable";
const filterValues: { text: string; value: string }[] = [
{
text: "Description",
value: "description",
},
{
text: "Availabilities",
value: "availabilities",
},
{
text: "Tasks",
value: "tasks",
},
];
export default function Events() { export default function Events() {
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false); const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
const [filter, setFilter] = useState<string | number>(""); const [filter, setFilter] = useState<string | number>("all");
const [contentFilter, setContentFilter] = useState(["description", "tasks"]);
const user = zustand((state) => state.user); const user = zustand((state) => state.user);
const events = useAsyncList({ const events = useAsyncList({
async load() { async load() {
const result = await apiCall<EventDataWithAvailability[]>( const result = await apiCall<EventDataWithAvailabilityAvailabilities[]>(
"GET", "GET",
"events/user/assignmentAvailability", "events/user/assignmentAvailability",
); );
@@ -45,7 +71,15 @@ export default function Events() {
}, },
}); });
function showEvent(event: EventDataWithAvailability): boolean { const availabilites = useAsyncList({
async load() {
return {
items: await getAvailabilities(),
};
},
});
function showEvent(event: EventDataWithAvailabilityAvailabilities): boolean {
switch (filter) { switch (filter) {
case "assigned": case "assigned":
return event.tasks.some((t) => { return event.tasks.some((t) => {
@@ -60,7 +94,9 @@ export default function Events() {
} }
} }
function showAvailabilitySelector(event: EventDataWithAvailability): boolean { function showAvailabilitySelector(
event: EventDataWithAvailabilityAvailabilities,
): boolean {
return event.tasks.some((t) => userTasks.items.includes(t.taskID)); return event.tasks.some((t) => userTasks.items.includes(t.taskID));
} }
@@ -68,6 +104,7 @@ export default function Events() {
<div className="relative flex flex-1 flex-col gap-4"> <div className="relative flex flex-1 flex-col gap-4">
<h2 className="text-center text-4xl">Upcoming Events</h2> <h2 className="text-center text-4xl">Upcoming Events</h2>
<div className="relative flex w-full">
<Tabs <Tabs
selectedKey={filter} selectedKey={filter}
onSelectionChange={setFilter} onSelectionChange={setFilter}
@@ -78,20 +115,57 @@ export default function Events() {
<Tab key="pending" title="Pending" /> <Tab key="pending" title="Pending" />
<Tab key="assigned" title="Assigned" /> <Tab key="assigned" title="Assigned" />
</Tabs> </Tabs>
<div className="absolute right-0">
<Popover placement="bottom-end">
<PopoverTrigger>
<Button isIconOnly>
<Filter className="cursor-pointer" />
</Button>
</PopoverTrigger>
<PopoverContent>
<CheckboxGroup
value={contentFilter}
onValueChange={setContentFilter}
>
{filterValues.map((f) => (
<Checkbox key={f.value} value={f.value}>
{f.text}
</Checkbox>
))}
</CheckboxGroup>
</PopoverContent>
</Popover>
</div>
</div>
<div className="mx-auto flex flex-wrap gap-4"> <div className="mx-auto flex flex-wrap gap-4">
{events.items.filter(showEvent).map((e) => ( {availabilites.items.length > 0
<Event key={e.eventID} event={e}> ? events.items.filter(showEvent).map((e) => (
<AssignmentTable <Event
key={e.eventID}
event={e}
hideDescription={!contentFilter.includes("description")}
>
<div className="mt-auto flex flex-col gap-4">
{contentFilter.includes("availabilities") ? (
<AvailabilityTable availabilities={e.availabilities} />
) : null}
{contentFilter.includes("tasks") ? (
<AssigmentTable
highlightUser={user?.userName} highlightUser={user?.userName}
tasks={e.tasks} tasks={e.tasks}
className="mt-auto"
/> />
) : null}
</div>
{showAvailabilitySelector(e) ? ( {showAvailabilitySelector(e) ? (
<AvailabilitySelector event={e} startSelection={e.availability} /> <AvailabilitySelector
event={e}
startSelection={e.availability}
/>
) : undefined} ) : undefined}
</Event> </Event>
))} ))
: null}
</div> </div>
{user?.admin ? ( {user?.admin ? (

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

12
client/src/app/icon.svg Executable file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
</style>
<title>calendar</title>
<path d="M26,4h-4V2h-2v2h-8V2h-2v2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6C28,4.9,27.1,4,26,4z M26,26H6V12h20
V26z M26,10H6V6h4v2h2V6h8v2h2V6h4V10z"/>
<rect id="_Transparent_Rectangle_" class="st0" width="32" height="32"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -3,21 +3,21 @@ import { color2Tailwind } from "./Colorselector";
import { Availability } from "@/app/admin/(availabilities)/AvailabilityEditor"; import { Availability } from "@/app/admin/(availabilities)/AvailabilityEditor";
export default function AvailabilityChip({ export default function AvailabilityChip({
availability, children,
className, className,
}: { }: {
availability?: Availability; children?: Availability;
className?: string; className?: string;
classNames?: ChipProps["classNames"]; classNames?: ChipProps["classNames"];
}) { }) {
return !!availability ? ( return !!children ? (
<Chip <Chip
classNames={{ classNames={{
base: `bg-${color2Tailwind(availability.color)}`, base: `bg-${color2Tailwind(children.color)}`,
}} }}
className={className} className={className}
> >
{availability.availabilityName} {children.availabilityName}
</Chip> </Chip>
) : null; ) : null;
} }

View File

@@ -1,13 +1,6 @@
import { classNames } from "@/lib"; import { classNames } from "@/lib";
import { EventData } from "@/Zustand"; import { EventData } from "@/Zustand";
import { import { Fragment } from "react";
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@heroui/react";
export default function AssignmentTable({ export default function AssignmentTable({
tasks, tasks,
@@ -23,40 +16,27 @@ export default function AssignmentTable({
return ( return (
<div className={className}> <div className={className}>
<h4 id="assignmentTableHeader">Tasks</h4> <h4 id="assignmentTableHeader">Tasks</h4>
<Table <div className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2">
aria-labelledby="assignmentTableHeader" {tasks.map((task) => (
hideHeader <Fragment key={task.taskID}>
removeWrapper <div
classNames={{ className={classNames(
td: "text-base", classNames({
}}
shadow="none"
title="Tasks"
>
<TableHeader>
<TableColumn>Task</TableColumn>
<TableColumn>Volunteer</TableColumn>
</TableHeader>
<TableBody items={tasks}>
{(task) => (
<TableRow
key={task.taskID}
className={classNames({
"text-danger": "text-danger":
task.userName === highlightUser || task.userName === highlightUser ||
task.taskName === highlightTask, task.taskName === highlightTask,
})} }),
"text-sm font-bold",
)}
> >
<TableCell className="font-bold">{task.taskName}</TableCell> {task.taskName}
<TableCell> </div>
{task.userName ?? ( {task.userName ?? (
<span className="italic text-highlight">missing</span> <span className="text-sm italic text-highlight">missing</span>
)} )}
</TableCell> </Fragment>
</TableRow> ))}
)} </div>
</TableBody>
</Table>
</div> </div>
); );
} }

View File

@@ -9,10 +9,12 @@ export default function AvailabilitySelector({
event, event,
className, className,
startSelection, startSelection,
onRefresh,
}: { }: {
event: EventAvailability; event: EventAvailability;
className?: string; className?: string;
startSelection?: number; startSelection?: number;
onRefresh?: () => void;
}) { }) {
const [value, setValue] = useState<Selection>(new Set([])); const [value, setValue] = useState<Selection>(new Set([]));
@@ -38,12 +40,16 @@ export default function AvailabilitySelector({
}); });
async function setAvailability(eventID: number, availabilityID: number) { async function setAvailability(eventID: number, availabilityID: number) {
await apiCall( const request = await apiCall(
"PUT", "PUT",
"events/user/availability", "events/user/availability",
{ eventID }, { eventID },
availabilityID, availabilityID,
); );
if (request.ok) {
onRefresh?.();
}
} }
return ( return (
@@ -59,7 +65,7 @@ export default function AvailabilitySelector({
<div> <div>
{availability.map((a) => {availability.map((a) =>
!!a.data ? ( !!a.data ? (
<AvailabilityChip key={a.key} availability={a.data} /> <AvailabilityChip key={a.key}>{a.data}</AvailabilityChip>
) : null, ) : null,
)} )}
</div> </div>
@@ -75,7 +81,7 @@ export default function AvailabilitySelector({
key={availability.availabilityID} key={availability.availabilityID}
textValue={availability.availabilityName} textValue={availability.availabilityName}
> >
<AvailabilityChip availability={availability} /> <AvailabilityChip>{availability}</AvailabilityChip>
</SelectItem> </SelectItem>
)} )}
</Select> </Select>

View File

@@ -0,0 +1,39 @@
import { useAsyncList } from "@react-stately/data";
import AvailabilityChip from "../AvailabilityChip";
import { EventAvailabilities } from "@/Zustand";
import { getAvailabilities } from "@/lib";
import { Fragment } from "react";
export default function AvailabilityTable({
className,
availabilities: eventAvailabilities,
}: {
className?: string;
availabilities: EventAvailabilities["availabilities"];
}) {
const availabilities = useAsyncList({
async load() {
return {
items: await getAvailabilities(),
};
},
});
return (
<div className={className}>
<h4>Availabilities</h4>
<div className="grid grid-cols-[auto_1fr] items-center gap-2">
{Object.entries(eventAvailabilities).map(([a, users]) => (
<Fragment key={a}>
<AvailabilityChip className="mx-auto">
{availabilities.items.find(
(i) => i.availabilityID === parseInt(a),
)}
</AvailabilityChip>
<div className="text-sm italic">{users.join(", ")}</div>
</Fragment>
))}
</div>
</div>
);
}

View File

@@ -7,9 +7,11 @@ import React from "react";
export default function Event({ export default function Event({
event, event,
hideDescription,
children, children,
}: { }: {
event: BaseEvent; event: BaseEvent;
hideDescription?: boolean;
children?: React.ReactNode | [React.ReactNode, React.ReactNode]; children?: React.ReactNode | [React.ReactNode, React.ReactNode];
}) { }) {
return ( return (
@@ -34,7 +36,7 @@ export default function Event({
</CardHeader> </CardHeader>
<Divider /> <Divider />
<CardBody> <CardBody>
{event.description != "" ? ( {event.description != "" && !hideDescription ? (
<div> <div>
<h4>Description</h4> <h4>Description</h4>
<div className="ms-2 mt-2">{event.description}</div> <div className="ms-2 mt-2">{event.description}</div>

View File

@@ -194,6 +194,8 @@ export async function getAvailabilities(): Promise<Availability[]> {
state.patch({ availabilities }); state.patch({ availabilities });
console.debug(zustand.getState().availabilities);
return availabilities; return availabilities;
} else { } else {
return []; return [];