fixed assignments view once again

This commit is contained in:
z1glr
2025-01-21 09:41:27 +00:00
parent c9fb212386
commit 67a4001883
29 changed files with 695 additions and 449 deletions

View File

@@ -0,0 +1,23 @@
import { Chip, ChipProps } from "@heroui/react";
import { color2Tailwind } from "./Colorselector";
import { Availability } from "@/app/admin/(availabilities)/AvailabilityEditor";
export default function AvailabilityChip({
availability,
className,
}: {
availability: Availability;
className?: string;
classNames?: ChipProps["classNames"];
}) {
return (
<Chip
classNames={{
base: `bg-${color2Tailwind(availability.color)}`,
}}
className={className}
>
{availability.name}
</Chip>
);
}

View File

@@ -0,0 +1,49 @@
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from "@heroui/react";
import React from "react";
import { TrashCan } from "@carbon/icons-react";
export default function DeleteConfirmation(props: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
children: React.ReactNode;
header: React.ReactNode;
onDelete?: () => void;
}) {
return (
<Modal
isOpen={props.isOpen}
onOpenChange={(isOpen) => {
props.onOpenChange(isOpen);
}}
shadow={"none" as "sm"}
backdrop="blur"
className="bg-accent-5"
>
<ModalContent>
<ModalHeader>
<h1 className="text-2xl">{props.header}</h1>
</ModalHeader>
<ModalBody>{props.children}</ModalBody>
<ModalFooter>
<Button variant="bordered" onPress={() => props.onOpenChange(false)}>
Cancel
</Button>
<Button
startContent={<TrashCan />}
color="danger"
onPress={() => props.onDelete?.()}
>
Delete event
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -1,7 +1,7 @@
import { Button } from "@heroui/react";
import EditEvent, { EventSubmitData } from "./EditEvent";
import { apiCall } from "@/lib";
import { AddLarge } from "@carbon/icons-react";
import EventEditor, { EventSubmitData } from "./EventEditor";
export default function AddEvent(props: {
className?: string;
@@ -20,8 +20,9 @@ export default function AddEvent(props: {
}
return (
<EditEvent
<EventEditor
{...props}
header="Add Event"
onSubmit={(data) => void addEvent(data)}
footer={
<Button
@@ -33,8 +34,6 @@ export default function AddEvent(props: {
Add
</Button>
}
>
Add Event
</EditEvent>
/>
);
}

View File

@@ -1,202 +1,35 @@
import React, { useEffect, useReducer, useState } from "react";
import {
getLocalTimeZone,
now,
parseDateTime,
toZoned,
ZonedDateTime,
} from "@internationalized/date";
import {
Checkbox,
CheckboxGroup,
DatePicker,
Form,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spinner,
Textarea,
} from "@heroui/react";
import { getTaskMap, Task } from "@/lib";
import React from "react";
import { apiCall } from "@/lib";
import { EventData } from "@/Zustand";
export interface EventSubmitData {
id: number;
date: string;
description: string;
tasks: number[];
}
interface State {
date: ZonedDateTime;
description: string;
tasks: string[];
}
import EventEditor, { EventSubmitData } from "./EventEditor";
export default function EditEvent(props: {
children: React.ReactNode;
footer: React.ReactNode;
initialState?: EventData;
value?: EventData;
className?: string;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onSubmit?: (data: EventSubmitData) => void;
onSuccess?: () => void;
}) {
const [reverseTasksMap, setReverseTasksMap] = useState<
Record<string, string>
>({});
const [state, dispatchState] = useReducer(
dispatchStateHandler,
dispatchStateHandler({} as State, { action: "reset" }),
);
const [tasksMap, setTasksMap] = useState<Record<number, Task>>({});
// initialize the state
function initialState(): State {
if (props.initialState !== undefined && reverseTasksMap !== undefined) {
const { description, date, tasks } = props.initialState;
return {
description,
date: toZoned(parseDateTime(date), getLocalTimeZone()),
tasks: Object.keys(tasks).map((task) => reverseTasksMap[task]),
};
} else {
return {
date: now(getLocalTimeZone()),
description: "",
tasks: [],
};
}
}
// update the state if the initialState-prop changes
useEffect(() => {
if (props.initialState !== undefined) {
dispatchState({ action: "reset" });
}
}, [props.initialState]);
// handle dispatch-calls
function dispatchStateHandler(
state: State,
args: { action: "patch" | "reset"; value?: Partial<State> },
): State {
if (args.action === "reset") {
return initialState();
} else {
return {
...state,
...args.value,
};
}
}
// shortcut for patching the state
function patchState(values: Partial<State>) {
dispatchState({ action: "patch", value: values });
}
// handle state dispatches
// get the available tasks and initialize the state with them
useEffect(() => {
(async () => {
const tasks = await getTaskMap();
setTasksMap(tasks);
setReverseTasksMap(
Object.fromEntries(
Object.entries(tasks).map(([id, task]) => {
return [task.text, id];
}),
),
);
})();
}, []);
// sends the patch-event-request to the backend
function patchEvent() {
if (props.initialState !== undefined) {
const { description, tasks, date } = state;
async function patchEvent(data: EventSubmitData) {
const result = await apiCall("PATCH", "events", undefined, data);
const data: EventSubmitData = {
id: props.initialState?.id,
description,
tasks: tasks.map((task) => parseInt(task)),
date: date.toAbsoluteString().slice(0, -1),
};
props.onSubmit?.(data);
if (result.ok) {
props.onSuccess?.();
}
}
return (
<Modal
<EventEditor
value={props.value}
key={props.value?.id}
header="Edit Event"
isOpen={props.isOpen}
shadow={"none" as "sm"} // somehow "none" isn't allowed
onOpenChange={props.onOpenChange}
backdrop="blur"
classNames={{
base: "bg-accent-5 ",
}}
>
<Form
validationBehavior="native"
onSubmit={(e) => {
e.preventDefault();
void patchEvent();
}}
>
<ModalContent>
<ModalHeader>
<h1 className="text-center text-2xl">{props.children}</h1>
</ModalHeader>
<ModalBody>
<DatePicker
isRequired
label="Event date"
name="date"
variant="bordered"
hideTimeZone
granularity="minute"
value={state.date}
onChange={(date) => (!!date ? patchState({ date }) : null)}
/>
<Textarea
variant="bordered"
placeholder="Description"
name="description"
value={state.description}
onValueChange={(description) => patchState({ description })}
/>
<CheckboxGroup
name="tasks"
value={state.tasks}
onValueChange={(tasks) => patchState({ tasks })}
validate={(value) =>
value.length > 0 ? true : "Atleast one task must be selected"
}
>
{tasksMap !== undefined ? (
Object.entries(tasksMap)
.filter(([, task]) => task.enabled)
.map(([id, task]) => (
<div key={id}>
<Checkbox value={id}>{task.text}</Checkbox>
</div>
))
) : (
<Spinner label="Loading" />
)}
</CheckboxGroup>
</ModalBody>
<ModalFooter>{props.footer}</ModalFooter>
</ModalContent>
</Form>
</Modal>
footer={props.footer}
onSubmit={patchEvent}
/>
);
}

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState } from "react";
import {
getLocalTimeZone,
now,
parseAbsoluteToLocal,
} from "@internationalized/date";
import {
Checkbox,
CheckboxGroup,
DatePicker,
DateValue,
Form,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spinner,
Textarea,
} from "@heroui/react";
import zustand, { EventData } from "@/Zustand";
export interface EventSubmitData {
id: number;
date: string;
description: string;
tasks: number[];
}
export default function EventEditor(props: {
header: React.ReactNode;
footer: React.ReactNode;
value?: EventData;
className?: string;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onSubmit?: (data: EventSubmitData) => void;
}) {
const [date, setDate] = useState<DateValue>(
!!props.value?.date
? parseAbsoluteToLocal(props.value?.date)
: now(getLocalTimeZone()),
);
const [description, setDescription] = useState(
props.value?.description ?? "",
);
const [eventTasks, setEventTasks] = useState<string[]>(
props.value?.tasks.map((k) => k.taskID.toString()) ?? [],
);
const tasks = zustand((state) => state.tasks);
function onSubmit() {
if (!!props.onSubmit) {
props.onSubmit({
id: props.value?.id ?? -1,
date: date.toAbsoluteString(),
description,
tasks: eventTasks.map((t) => parseInt(t)),
});
}
}
return (
<Modal
isOpen={props.isOpen}
shadow={"none" as "sm"} // somehow "none" isn't allowed
onOpenChange={props.onOpenChange}
backdrop="blur"
classNames={{
base: "bg-accent-5 ",
}}
>
<Form
validationBehavior="native"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<ModalContent>
<ModalHeader>
<h1 className="text-center text-2xl">{props.header}</h1>
</ModalHeader>
<ModalBody>
<DatePicker
isRequired
label="Event date"
name="date"
variant="bordered"
hideTimeZone
granularity="minute"
value={date}
onChange={setDate}
/>
<Textarea
variant="bordered"
placeholder="Description"
name="description"
value={description}
onValueChange={setDescription}
/>
<CheckboxGroup
name="tasks"
value={eventTasks}
onValueChange={setEventTasks}
validate={(value) =>
value.length > 0 ? true : "Atleast one task must be selected"
}
>
{!!tasks ? (
tasks
?.filter((task) => task.enabled)
.map((task) => (
<div key={task.id}>
<Checkbox value={task.id?.toString()}>
{task.name}
</Checkbox>
</div>
))
) : (
<Spinner label="Loading" />
)}
</CheckboxGroup>
</ModalBody>
<ModalFooter>{props.footer}</ModalFooter>
</ModalContent>
</Form>
</Modal>
);
}

View File

@@ -1,7 +1,7 @@
"use local";
import { DateFormatter } from "@/lib";
import { getLocalTimeZone, parseDateTime } from "@internationalized/date";
import { parseAbsoluteToLocal } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";
export default function LocalDate(props: {
@@ -14,9 +14,7 @@ export default function LocalDate(props: {
return (
<span className={props.className}>
{props.children !== undefined
? formatter.format(
parseDateTime(props.children).toDate(getLocalTimeZone()),
)
? formatter.format(parseAbsoluteToLocal(props.children).toDate())
: ""}
</span>
);