added availability changing to events view

This commit is contained in:
z1glr
2025-01-24 12:35:05 +00:00
parent c1020d26b2
commit ea3e4573b3
10 changed files with 207 additions and 71 deletions

View File

@@ -35,6 +35,11 @@ type EventWithAvailabilities struct {
Availabilities availabilities.AvailabilityMap `json:"availabilities"` Availabilities availabilities.AvailabilityMap `json:"availabilities"`
} }
type EventWithAssignmentsUserAvailability struct {
EventWithAssignments
Availability *int `json:"availability" db:"availabilityID"`
}
type EventCreate struct { type EventCreate struct {
Date string `db:"date" json:"date" validate:"required,datetime=2006-01-02T15:04:05.999999999Z"` Date string `db:"date" json:"date" validate:"required,datetime=2006-01-02T15:04:05.999999999Z"`
Description string `db:"description" json:"description"` Description string `db:"description" json:"description"`
@@ -54,6 +59,19 @@ func (e EventData) WithAssignments() (EventWithAssignments, error) {
} }
} }
func (e EventWithAssignments) WithUserAvailability(userName string) (EventWithAssignmentsUserAvailability, error) {
// get the availability of the user
event := EventWithAssignmentsUserAvailability{
EventWithAssignments: e,
}
if err := db.DB.Select(&event, "SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", e.EventID, userName); err != nil {
return EventWithAssignmentsUserAvailability{}, err
} else {
return event, nil
}
}
func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) { func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) {
// get the event with assignments // get the event with assignments
if event, err := e.WithAssignments(); err != nil { if event, err := e.WithAssignments(); err != nil {
@@ -217,6 +235,26 @@ func WithAvailabilities() ([]EventWithAvailabilities, error) {
} }
} }
func WithUserAvailability(userName string) ([]EventWithAssignmentsUserAvailability, error) {
var events []EventWithAssignmentsUserAvailability
if err := db.DB.Select(&events, "SELECT EVENTS.eventID, EVENTS.description, EVENTS.date, USER_AVAILABILITIES.availabilityID FROM EVENTS LEFT JOIN USER_AVAILABILITIES ON EVENTS.eventID = USER_AVAILABILITIES.eventID AND USER_AVAILABILITIES.userName = $1", userName); err != nil {
return nil, err
} else {
// get the assignments for every event
for ii, event := range events {
if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAssignments(); err != nil {
// remove the current event from the events
events = append(events[:ii], events[ii+1:]...)
} else {
events[ii].EventWithAssignments = eventWithAssignments
}
}
return events, nil
}
}
func UserPending(userName string) ([]EventData, error) { func UserPending(userName string) ([]EventData, error) {
var result []EventData var result []EventData

View File

@@ -90,6 +90,17 @@ func (a *Handler) getEventsAvailabilities() {
} }
} }
func (a *Handler) getEventUserAssignmentAvailability() {
// retrieve the assignments
if events, err := events.WithUserAvailability(a.UserName); err != nil {
a.Status = fiber.StatusBadRequest
logger.Log().Msgf("getting events with tasks and user-availability failed: %v", err)
} else {
a.Data = events
}
}
func (a *Handler) getEventsUserPending() { func (a *Handler) getEventsUserPending() {
if events, err := events.UserPending(a.UserName); err != nil { if events, err := events.UserPending(a.UserName); err != nil {
a.Status = fiber.StatusInternalServerError a.Status = fiber.StatusInternalServerError

View File

@@ -83,6 +83,9 @@ func init() {
// all events with the availabilities of the individual users // all events with the availabilities of the individual users
"events/availabilities": (*Handler).getEventsAvailabilities, "events/availabilities": (*Handler).getEventsAvailabilities,
// all events with the task-assignments and the availability of the current user
"events/user/assignmentAvailability": (*Handler).getEventUserAssignmentAvailability,
// events the user has to enter his availability for // events the user has to enter his availability for
"events/user/pending": (*Handler).getEventsUserPending, "events/user/pending": (*Handler).getEventsUserPending,

View File

@@ -10,10 +10,16 @@ export interface BaseEvent {
description: string; description: string;
} }
export type EventAvailability = BaseEvent & {
availability: number;
};
export type EventData = BaseEvent & { export type EventData = BaseEvent & {
tasks: TaskAssignment[]; tasks: TaskAssignment[];
}; };
export type EventDataWithAvailability = EventData & EventAvailability;
export interface TaskAssignment { export interface TaskAssignment {
taskID: number; taskID: number;
taskName: string; taskName: string;

View File

@@ -1,5 +1,5 @@
import MyEvents from "./MyEvents"; import MyEvents from "./MyEvents";
import PengingEvents from "./PendingEvents"; import PendingEvents from "./PendingEvents";
export default function Overview() { export default function Overview() {
return ( return (
@@ -12,7 +12,7 @@ export default function Overview() {
<h1 className="mb-4 mt-8 text-center text-4xl lg:mt-0"> <h1 className="mb-4 mt-8 text-center text-4xl lg:mt-0">
Pending Events Pending Events
</h1> </h1>
<PengingEvents /> <PendingEvents />
</div> </div>
</div> </div>
); );

View File

@@ -1,17 +1,12 @@
"use client"; "use client";
import AvailabilityChip from "@/components/AvailabilityChip"; import AvailabilitySelector from "@/components/Event/AvailabilitySelector";
import Event from "@/components/Event/Event"; import Event from "@/components/Event/Event";
import { apiCall, getAvailabilities } from "@/lib"; import { apiCall } from "@/lib";
import { BaseEvent } from "@/Zustand"; import { EventAvailability } from "@/Zustand";
import { Select, SelectItem } from "@heroui/react";
import { useAsyncList } from "@react-stately/data"; import { useAsyncList } from "@react-stately/data";
type EventAvailability = BaseEvent & { export default function PendingEvents() {
availability: number;
};
export default function PengingEvents() {
// get the events the user hasn't yet inserted his availability for // get the events the user hasn't yet inserted his availability for
const events = useAsyncList({ const events = useAsyncList({
async load() { async load() {
@@ -32,56 +27,11 @@ export default function PengingEvents() {
}, },
}); });
// the individual, selectable availabilities
const availabilities = useAsyncList({
async load() {
return {
items: (await getAvailabilities()).filter((a) => a.enabled),
};
},
});
async function setAvailability(eventID: number, availabilityID: number) {
await apiCall(
"PUT",
"events/user/availability",
{ eventID },
availabilityID,
);
}
return ( return (
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
{events.items.map((e) => ( {events.items.map((e) => (
<Event key={e.eventID} event={e}> <Event key={e.eventID} event={e}>
<Select <AvailabilitySelector event={e} className="mt-auto" />
items={availabilities.items}
label="Availability"
variant="bordered"
className="mt-auto"
isMultiline
renderValue={(availability) => (
<div>
{availability.map((a) =>
!!a.data ? (
<AvailabilityChip key={a.key} availability={a.data} />
) : null,
)}
</div>
)}
onSelectionChange={(a) =>
setAvailability(e.eventID, parseInt(a.anchorKey ?? ""))
}
>
{(availability) => (
<SelectItem
key={availability.availabilityID}
textValue={availability.availabilityName}
>
<AvailabilityChip availability={availability} />
</SelectItem>
)}
</Select>
</Event> </Event>
))} ))}
</div> </div>

View File

@@ -3,23 +3,31 @@ import AddEvent from "@/components/Event/AddEvent";
import AssignmentTable from "@/components/Event/AssignmentTable"; import AssignmentTable from "@/components/Event/AssignmentTable";
import Event from "@/components/Event/Event"; import Event from "@/components/Event/Event";
import { apiCall } from "@/lib"; import { apiCall } from "@/lib";
import zustand, { EventData } from "@/Zustand"; import zustand, { EventDataWithAvailability } from "@/Zustand";
import { Add } from "@carbon/icons-react"; import { Add } from "@carbon/icons-react";
import { Button } from "@heroui/react"; import { Button, 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";
export default function Events() { export default function Events() {
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false); const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
const admin = zustand((state) => state.user?.admin); const [filter, setFilter] = useState<string | number>("");
const events = useAsyncList<EventData>({ const user = zustand((state) => state.user);
const events = useAsyncList({
async load() { async load() {
const result = await apiCall<EventData[]>("GET", "events/assignments"); const result = await apiCall<EventDataWithAvailability[]>(
"GET",
"events/user/assignmentAvailability",
);
if (result.ok) { if (result.ok) {
const data = await result.json(); const data = await result.json();
console.debug(data);
return { return {
items: data, items: data,
}; };
@@ -31,20 +39,52 @@ export default function Events() {
}, },
}); });
function showEvent(event: EventDataWithAvailability): boolean {
switch (filter) {
case "assigned":
return event.tasks.some((t) => {
return t.userName === user?.userName;
});
case "pending":
return event.availability === null;
default:
return true;
}
}
return ( return (
<div className="relative flex-1"> <div className="relative flex flex-1 flex-col gap-4">
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2> <h2 className="text-center text-4xl">Upcoming Events</h2>
<Tabs
selectedKey={filter}
onSelectionChange={setFilter}
color="primary"
className="mx-auto"
>
<Tab key="all" title="All" />
<Tab key="pending" title="Pending" />
<Tab key="assigned" title="Assigned" />
</Tabs>
<div className="flex flex-wrap justify-center gap-4"> <div className="flex flex-wrap justify-center gap-4">
{events.items.map((ee, ii) => ( {events.items.filter(showEvent).map((e) => (
<Event key={ii} event={ee}> <Event key={e.eventID} event={e}>
<div className="mt-auto"> <div className="mt-auto">
<AssignmentTable tasks={ee.tasks} /> <AvailabilitySelector
event={e}
className="mb-2"
startSelection={e.availability}
/>
<AssignmentTable tasks={e.tasks} />
</div> </div>
</Event> </Event>
))} ))}
</div> </div>
{admin ? ( {user?.admin ? (
<> <>
<Button <Button
color="primary" color="primary"

View File

@@ -5,13 +5,15 @@ export default function AssignmentTable({
tasks, tasks,
highlightTask, highlightTask,
highlightUser, highlightUser,
className,
}: { }: {
tasks: EventData["tasks"]; tasks: EventData["tasks"];
highlightUser?: string; highlightUser?: string;
highlightTask?: string; highlightTask?: string;
className?: string;
}) { }) {
return ( return (
<table> <table className={className}>
<tbody> <tbody>
{tasks.map((task) => ( {tasks.map((task) => (
<tr <tr

View File

@@ -0,0 +1,81 @@
import { Select, Selection, SelectItem } from "@heroui/react";
import AvailabilityChip from "../AvailabilityChip";
import { apiCall, getAvailabilities } from "@/lib";
import { useAsyncList } from "@react-stately/data";
import { EventAvailability } from "@/Zustand";
import { useState } from "react";
export default function AvailabilitySelector({
event,
className,
startSelection,
}: {
event: EventAvailability;
className?: string;
startSelection?: number;
}) {
const [value, setValue] = useState<Selection>(new Set([]));
// the individual, selectable availabilities
const availabilities = useAsyncList({
async load() {
const availabilities = (await getAvailabilities()).filter(
(a) => a.enabled,
);
// if the availabilities contain the startSelection, set it
if (
!!startSelection &&
availabilities.some((a) => a.availabilityID === startSelection)
) {
setValue(new Set([startSelection.toString()]));
}
return {
items: availabilities,
};
},
});
async function setAvailability(eventID: number, availabilityID: number) {
await apiCall(
"PUT",
"events/user/availability",
{ eventID },
availabilityID,
);
}
return (
<Select
items={availabilities.items}
label="Availability"
variant="bordered"
className={className}
isMultiline
renderValue={(availability) => (
<div>
{availability.map((a) =>
!!a.data ? (
<AvailabilityChip key={a.key} availability={a.data} />
) : null,
)}
</div>
)}
selectedKeys={value}
onSelectionChange={(a) => {
void setAvailability(event.eventID, parseInt(a.anchorKey ?? ""));
setValue(a);
}}
>
{(availability) => (
<SelectItem
key={availability.availabilityID}
textValue={availability.availabilityName}
>
<AvailabilityChip availability={availability} />
</SelectItem>
)}
</Select>
);
}

View File

@@ -2,7 +2,7 @@
import LocalDate from "../LocalDate"; import LocalDate from "../LocalDate";
import { BaseEvent } from "@/Zustand"; import { BaseEvent } from "@/Zustand";
import { Card, CardBody, CardHeader, Divider } from "@heroui/react"; import { Card, CardBody, CardHeader, Divider, Textarea } from "@heroui/react";
import React from "react"; import React from "react";
export default function Event({ export default function Event({
@@ -34,7 +34,12 @@ export default function Event({
</CardHeader> </CardHeader>
<Divider /> <Divider />
<CardBody> <CardBody>
<div>{event.description}</div> <Textarea
isReadOnly
label="Description"
defaultValue={event.description}
variant="bordered"
/>
{children} {children}
</CardBody> </CardBody>