feat: add parent announcements and family or child event responses
This commit is contained in:
@@ -1,15 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { EventParticipationStatus } from "@prisma/client";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { AlertTriangle, Check, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { submitEventRsvp } from "@/actions/rsvp";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { RsvpEventDto } from "@/lib/rsvp";
|
||||
import { RsvpDialog } from "@/components/rsvp-form";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -17,41 +12,18 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export type DashboardRsvpEventDto = {
|
||||
id: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
rsvpDeadline: string | null;
|
||||
children: {
|
||||
id: string;
|
||||
name: string;
|
||||
status: EventParticipationStatus | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function RsvpActionCard({ events }: { events: DashboardRsvpEventDto[] }) {
|
||||
export function RsvpActionCard({ events }: { events: RsvpEventDto[] }) {
|
||||
if (events.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card className="mb-8 border-[var(--warn)] bg-[var(--warn-soft)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg text-[var(--ink)]">
|
||||
<AlertTriangle className="h-5 w-5 text-[var(--warn)] [stroke-width:1.7]" />
|
||||
Aktion erforderlich
|
||||
<Card className="mb-5 border-[var(--warn)] bg-[var(--warn-soft)]">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<AlertTriangle className="h-5 w-5 text-[var(--warn)]" />
|
||||
Rückmeldung erforderlich
|
||||
</CardTitle>
|
||||
<CardDescription className="text-[var(--ink-soft)]">
|
||||
Für diese Termine fehlt noch mindestens eine Rückmeldung deiner
|
||||
Familie.
|
||||
<CardDescription>
|
||||
Für diese Termine fehlt noch eine Antwort eurer Familie.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
@@ -61,7 +33,7 @@ export function RsvpActionCard({ events }: { events: DashboardRsvpEventDto[] })
|
||||
className="flex flex-col gap-3 rounded-lg border border-[var(--hairline)] bg-[var(--surface)] p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-[var(--ink)]">{event.title}</p>
|
||||
<p className="font-medium">{event.title}</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-soft)]">
|
||||
Rückmeldung bis{" "}
|
||||
{format(
|
||||
@@ -78,130 +50,3 @@ export function RsvpActionCard({ events }: { events: DashboardRsvpEventDto[] })
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RsvpDialog({ event }: { event: DashboardRsvpEventDto }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const initialSelection = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
event.children.map((child) => [child.id, child.status ?? ""]),
|
||||
) as Record<string, EventParticipationStatus | "">,
|
||||
[event.children],
|
||||
);
|
||||
const [selection, setSelection] = useState(initialSelection);
|
||||
|
||||
function submit() {
|
||||
const missingChild = event.children.find((child) => !selection[child.id]);
|
||||
if (missingChild) {
|
||||
toast.error(`Bitte Rückmeldung für ${missingChild.name} auswählen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
for (const child of event.children) {
|
||||
const status = selection[child.id];
|
||||
if (!status) continue;
|
||||
|
||||
const result = await submitEventRsvp({
|
||||
eventId: event.id,
|
||||
childId: child.id,
|
||||
status,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Rückmeldung gespeichert.");
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) setSelection(initialSelection);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">Jetzt abstimmen</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rückmeldung für {event.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Wähle pro Kind aus, ob es am Termin teilnimmt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3 py-2">
|
||||
{event.children.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="font-medium text-[var(--ink)]">{child.name}</p>
|
||||
{child.status && (
|
||||
<Badge variant="outline">bereits gemeldet</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-lg border border-[var(--hairline)] bg-[var(--surface)] p-3 text-sm text-[var(--ink)] hover:bg-[var(--surface-2)]">
|
||||
<input
|
||||
type="radio"
|
||||
name={`rsvp-${child.id}`}
|
||||
checked={
|
||||
selection[child.id] ===
|
||||
EventParticipationStatus.ATTENDING
|
||||
}
|
||||
onChange={() =>
|
||||
setSelection((current) => ({
|
||||
...current,
|
||||
[child.id]: EventParticipationStatus.ATTENDING,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Check className="h-4 w-4 text-[var(--good)] [stroke-width:1.7]" />
|
||||
Nimmt teil
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-lg border border-[var(--hairline)] bg-[var(--surface)] p-3 text-sm text-[var(--ink)] hover:bg-[var(--surface-2)]">
|
||||
<input
|
||||
type="radio"
|
||||
name={`rsvp-${child.id}`}
|
||||
checked={
|
||||
selection[child.id] ===
|
||||
EventParticipationStatus.NOT_ATTENDING
|
||||
}
|
||||
onChange={() =>
|
||||
setSelection((current) => ({
|
||||
...current,
|
||||
[child.id]: EventParticipationStatus.NOT_ATTENDING,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<X className="h-4 w-4 text-[var(--danger)] [stroke-width:1.7]" />
|
||||
Nimmt nicht teil
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="button" disabled={isPending} onClick={submit}>
|
||||
{isPending ? "Speichern..." : "Rückmeldung speichern"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user