Files
kita-planer/src/app/dashboard/notdienst/plan/plan-view.tsx
T

806 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useMemo, useState, useTransition } from "react";
import Link from "next/link";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { NotdienstPlanStatus } from "@prisma/client";
import {
ArrowRight,
CheckSquare,
Download,
Loader2,
RotateCcw,
Send,
Wand2,
} from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
generatePlanAction,
publishPlanAction,
updateAssignmentAction,
sendNotdienstRemindersAction,
resetPlanAction,
} from "./actions";
interface PlanViewProps {
planId?: string;
remindersOpen: boolean;
status?: NotdienstPlanStatus;
days: {
date: string;
assignmentId: string | null;
activeAlarmId?: string | null;
canAlarm?: boolean;
childId: string | null;
isOriginallyUnassigned?: boolean;
isInvalid?: boolean;
}[];
allChildren: {
id: string;
name: string;
parentName: string;
familyId: string;
}[];
familyStatuses: {
id: string;
name: string;
childCount: number;
requiredDays: number;
selectedDays: {
iso: string;
label: string;
}[];
}[];
requiredDays: number;
respondedFamilies: number;
openFamilies: number;
totalFamilies: number;
totalAvailabilitySlots: number;
coverageFactor: number;
closureCount: number;
monthName: string;
year: number;
month: number;
}
export function PlanView({
planId,
remindersOpen,
status,
days,
allChildren,
familyStatuses,
requiredDays,
respondedFamilies,
openFamilies,
totalFamilies,
totalAvailabilitySlots,
coverageFactor,
closureCount,
monthName,
year,
month,
}: PlanViewProps) {
const [isPending, startTransition] = useTransition();
const [activeTab, setActiveTab] = useState<"preview" | "manual">("preview");
const [resetDialogOpen, setResetDialogOpen] = useState(false);
const responsePercent =
totalFamilies > 0
? Math.round((respondedFamilies / totalFamilies) * 100)
: 0;
const handleGenerate = () => {
startTransition(async () => {
const result = await generatePlanAction(year, month);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else {
toast.success("Plan erfolgreich generiert.");
}
});
};
const handlePublish = () => {
if (!planId) return;
startTransition(async () => {
const result = await publishPlanAction(planId);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else if ("success" in result && result.success) {
if (result.failed || ("deferred" in result && result.deferred))
toast.warning(
"Plan veröffentlicht. Einige E-Mails stehen noch aus – siehe Versandstatus.",
);
else
toast.success(
"Plan veröffentlicht. Die Benachrichtigungen wurden vorbereitet.",
);
}
});
};
const handleSendReminders = () => {
startTransition(async () => {
const result = await sendNotdienstRemindersAction(year, month);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else if ("success" in result && result.success) {
if (result.failed || ("deferred" in result && result.deferred))
toast.warning(
`${result.count} E-Mails versendet. Offene Nachrichten bleiben gespeichert – siehe Versandstatus.`,
);
else
toast.success(
`${result.count} E-Mails versendet. Bereits heute erinnerte Haushalte erhalten keine zweite Erinnerung.`,
);
}
});
};
const handleReset = () => {
if (!planId) return;
startTransition(async () => {
const result = await resetPlanAction(planId);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else {
toast.success("Plan wurde erfolgreich zurückgesetzt.");
setResetDialogOpen(false);
}
});
};
const handleAssignmentChange = (date: string, newChildId: string) => {
if (!planId || (status !== "DRAFT" && status !== "PUBLISHED")) return;
const valueToSet = newChildId === "empty" ? null : newChildId;
startTransition(async () => {
const result = await updateAssignmentAction(
planId,
date.slice(0, 10),
valueToSet,
);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else {
toast.success("Zuteilung aktualisiert.");
}
});
};
if (!planId && totalAvailabilitySlots === 0) {
return (
<Card className="border-dashed">
<div className="flex min-h-[280px] flex-col items-center justify-center p-8 text-center">
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-[var(--accent-soft)] text-[var(--accent-deep)]">
<Wand2 className="h-7 w-7 [stroke-width:1.7]" />
</div>
<h3 className="mb-1 text-xl font-medium tracking-[-0.02em] text-[var(--ink)]">
Noch keine Verfügbarkeiten
</h3>
<p className="mb-5 max-w-md text-sm leading-6 text-[var(--ink-soft)]">
Für {monthName} hat noch keine Familie geantwortet. Verschicke
zuerst Erinnerungen, damit die automatische Planung belastbar ist.
</p>
<div className="flex flex-wrap justify-center gap-2">
<Button disabled={isPending} onClick={handleGenerate}>
Trotzdem generieren
</Button>
<Button
variant="ghost"
onClick={handleSendReminders}
disabled={isPending || !remindersOpen}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
) : (
<Send className="h-4 w-4 [stroke-width:1.7]" />
)}
Erinnerungen verschicken
</Button>
</div>
</div>
</Card>
);
}
if (!planId) {
return (
<div className="space-y-5">
<div className="grid grid-cols-[minmax(0,2fr)_minmax(300px,1fr)] gap-5 max-[1050px]:grid-cols-1">
<Card className="p-0">
<div className="border-b border-[var(--hairline-soft)] px-6 py-5">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
Verfügbarkeiten Eltern
</h2>
<p className="mt-1 text-[13.5px] leading-5 text-[var(--ink-soft)]">
{respondedFamilies} von {totalFamilies} Familien haben
vollständig geantwortet · noch {openFamilies} offen
</p>
</div>
<span className="text-sm font-semibold tabular-nums text-[var(--ink-soft)]">
{responsePercent}%
</span>
</div>
<div className="mt-4 h-1 overflow-hidden rounded-full bg-[var(--accent-soft)]">
<div
className="h-full rounded-full bg-[var(--accent)]"
style={{ width: `${responsePercent}%` }}
/>
</div>
</div>
<div className="divide-y divide-[var(--hairline-soft)]">
{familyStatuses.map((family) => (
<div
key={family.id}
className="group relative flex items-center gap-3 px-6 py-3.5 transition-colors hover:bg-[var(--surface-2)]"
>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-[11px] font-semibold uppercase text-[var(--accent-deep)]">
{family.name.slice(0, 1)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-[var(--ink)]">
{family.name}
</p>
<p className="text-xs text-[var(--ink-muted)]">
{family.childCount} Kind
{family.childCount === 1 ? "" : "er"}
</p>
</div>
<FamilyStatusPill
selectedDays={family.selectedDays.length}
requiredDays={family.requiredDays}
/>
<div className="pointer-events-none absolute right-6 top-10 z-20 hidden max-w-xs rounded-lg bg-[oklch(0.22_0.015_45)] px-3 py-2 text-xs leading-5 text-white shadow-lg group-hover:block">
{family.selectedDays.length > 0
? family.selectedDays.map((day) => day.label).join(" · ")
: "Noch keine Tage markiert"}
</div>
</div>
))}
</div>
<div className="border-t border-[var(--hairline-soft)] px-6 py-4">
<Button
variant="ghost"
onClick={handleSendReminders}
disabled={isPending || openFamilies === 0 || !remindersOpen}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
) : (
<Send className="h-4 w-4 [stroke-width:1.7]" />
)}
Erinnerung an offene senden ({openFamilies})
</Button>
</div>
</Card>
<Card className="p-6">
<div>
<h2 className="text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
Bedarfs-Übersicht
</h2>
<p className="mt-1 text-[13.5px] text-[var(--ink-soft)]">
Planungsgrundlage für {monthName}.
</p>
</div>
<div className="mt-5 grid gap-4">
<PlanMetric
label={`Tage benötigt in ${monthName}`}
value={requiredDays}
/>
<PlanMetric
label="Verfügbarkeiten gesamt"
value={totalAvailabilitySlots}
/>
<PlanMetric
label="Überdeckung"
value={`${coverageFactor.toFixed(1)}×`}
badge="good"
/>
<PlanMetric label="Schließtage" value={closureCount} />
</div>
<div className="mt-5 rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] px-4 py-3 text-sm leading-6 text-[var(--ink-soft)]">
{coverageFactor >= 1
? "Du hast genug Verfügbarkeiten für einen Plan."
: "Die Verfügbarkeiten reichen noch nicht für einen belastbaren Plan."}
</div>
</Card>
</div>
<Card variant="featured" className="sticky bottom-6 z-10 p-5">
<div className="flex items-center justify-between gap-5 max-[760px]:flex-col max-[760px]:items-start">
<div className="flex gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-[var(--accent-deep)]">
<Wand2 className="h-5 w-5 [stroke-width:1.7]" />
</div>
<div>
<h3 className="text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
Plan automatisch generieren
</h3>
<p className="mt-1 text-[13.5px] text-[var(--ink-soft)]">
Verteilt {requiredDays} Notdiensttage fair auf die verfügbaren
Familien.
</p>
<details className="mt-3 text-xs text-[var(--ink-soft)]">
<summary className="cursor-pointer font-semibold text-[var(--accent-deep)]">
Faire Verteilung berücksichtigt
</summary>
<div className="mt-2 flex flex-wrap gap-2">
<span>☑ Quote pro Familie</span>
<span>☑ Bevorzugte Tage</span>
<span>☐ Geschwister-Familien zusammen</span>
</div>
</details>
</div>
</div>
<Button onClick={handleGenerate} disabled={isPending}>
{isPending && (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
)}
Generieren
<ArrowRight className="h-4 w-4 [stroke-width:1.7]" />
</Button>
</div>
</Card>
</div>
);
}
const isDraft = status === "DRAFT";
return (
<div className="space-y-5">
<div className="print:hidden flex flex-wrap items-center justify-between gap-3">
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
<TabButton
active={activeTab === "preview"}
onClick={() => setActiveTab("preview")}
>
Plan-Vorschau
</TabButton>
<TabButton
active={activeTab === "manual"}
onClick={() => setActiveTab("manual")}
withDivider
>
Manuell zuweisen
</TabButton>
</div>
<div className="flex flex-wrap gap-2">
{isDraft && (
<Button onClick={handlePublish} disabled={isPending}>
{isPending && (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
)}
<CheckSquare className="h-4 w-4 [stroke-width:1.7]" />
Plan veröffentlichen
</Button>
)}
<Button variant="ghost" onClick={() => window.print()}>
<Download className="h-4 w-4 [stroke-width:1.7]" />
Als PDF exportieren
</Button>
<Button
variant="ghost"
className="text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => setResetDialogOpen(true)}
disabled={isPending}
>
<RotateCcw className="h-4 w-4 [stroke-width:1.7]" />
Plan zurücksetzen
</Button>
</div>
</div>
<Card className="print:hidden flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Badge variant={isDraft ? "warning" : "success"}>
{isDraft ? "Entwurf" : "Veröffentlicht"}
</Badge>
<span className="text-sm text-[var(--ink-soft)]">
{isDraft
? "Du kannst die Liste jetzt manuell anpassen."
: "Dieser Plan ist veröffentlicht und kann bei Änderungen nachbearbeitet werden."}
</span>
</div>
</Card>
{activeTab === "preview" ? (
<PlanPreviewTable days={days} allChildren={allChildren} />
) : (
<ManualAssignmentTable
days={days}
allChildren={allChildren}
familyStatuses={familyStatuses}
isDraft={isDraft}
isPending={isPending}
onAssignmentChange={handleAssignmentChange}
/>
)}
<Dialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Plan zurücksetzen?</DialogTitle>
<DialogDescription>
Möchtest du den Plan wirklich zurücksetzen? Alle Zuteilungen für
diesen Monat gehen unwiderruflich verloren.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setResetDialogOpen(false)}
disabled={isPending}
>
Abbrechen
</Button>
<Button
type="button"
variant="destructive"
onClick={handleReset}
disabled={isPending}
>
{isPending ? "Zurücksetzen..." : "Ja, Plan zurücksetzen"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
function FamilyStatusPill({
selectedDays,
requiredDays,
}: {
selectedDays: number;
requiredDays: number;
}) {
if (selectedDays === 0) {
return (
<span className="rounded-full border border-[var(--warn)] bg-[var(--warn-soft)] px-2.5 py-1 text-[11px] font-semibold text-[var(--warn)]">
Noch nicht
</span>
);
}
return (
<span
className={
selectedDays >= requiredDays
? "rounded-full border border-[var(--good)] bg-[var(--good-soft)] px-2.5 py-1 text-[11px] font-semibold text-[var(--good)]"
: "rounded-full border border-[var(--warn)] bg-[var(--warn-soft)] px-2.5 py-1 text-[11px] font-semibold text-[var(--warn)]"
}
>
{selectedDays} / {requiredDays} Tage
</span>
);
}
function PlanMetric({
label,
value,
badge,
}: {
label: string;
value: number | string;
badge?: "good";
}) {
return (
<div className="flex items-center justify-between gap-4 border-b border-[var(--hairline-soft)] pb-3 last:border-b-0 last:pb-0">
<span className="text-[13.5px] text-[var(--ink-soft)]">{label}</span>
{badge ? (
<span className="rounded-full border border-[var(--good)] bg-[var(--good-soft)] px-2.5 py-1 text-sm font-semibold text-[var(--good)] tabular-nums">
{value}
</span>
) : (
<span className="text-2xl font-medium tracking-[-0.025em] text-[var(--ink)] tabular-nums">
{value}
</span>
)}
</div>
);
}
function TabButton({
active,
withDivider,
onClick,
children,
}: {
active: boolean;
withDivider?: boolean;
onClick: () => void;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className={[
"h-9 px-4 text-[13px] font-semibold transition focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)]",
withDivider ? "border-l border-[var(--hairline-soft)]" : "",
active
? "bg-[var(--accent-soft)] text-[var(--accent-deep)]"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)]",
].join(" ")}
>
{children}
</button>
);
}
function PlanPreviewTable({
days,
allChildren,
}: Pick<PlanViewProps, "days" | "allChildren">) {
return (
<Card className="overflow-hidden p-0">
<div className="grid grid-cols-[1.1fr_1fr_2fr_1.6fr_1fr] border-b border-[var(--hairline-soft)] bg-[var(--surface-2)] p-4 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
<div>Datum</div>
<div>Wochentag</div>
<div>Zugewiesen</div>
<div>Backup</div>
<div>Status</div>
</div>
<div className="divide-y divide-[var(--hairline-soft)]">
{days.map((day) => {
const [yr, mo, dy] = day.date.slice(0, 10).split("-").map(Number);
const dateObj = new Date(yr, mo - 1, dy);
const assignedChild = allChildren.find(
(child) => child.id === day.childId,
);
return (
<div
key={day.date}
className="grid cursor-pointer grid-cols-[1.1fr_1fr_2fr_1.6fr_1fr] items-center p-4 text-sm transition-colors hover:bg-[var(--surface-2)]"
>
<div className="font-medium tabular-nums text-[var(--ink)]">
{format(dateObj, "dd.MM.", { locale: de })}
</div>
<div className="text-[var(--ink-soft)]">
{format(dateObj, "EEEE", { locale: de })}
</div>
<div className="text-[var(--ink)]">
{assignedChild
? assignedChild.parentName
: day.childId
? "Inaktives Kind"
: "Unbesetzt"}
{assignedChild && (
<span className="ml-1 text-[var(--ink-muted)]">
({assignedChild.name})
</span>
)}
{day.activeAlarmId ? (
<Link
className="mt-2 block text-xs font-semibold text-[var(--accent-deep)] hover:underline"
href={`/dashboard/notdienst/alarm?alarm=${day.activeAlarmId}`}
>
Aktiven Alarm öffnen
</Link>
) : day.canAlarm && day.assignmentId ? (
<Link
className="mt-2 block text-xs font-semibold text-[var(--accent-deep)] hover:underline"
href={`/dashboard/notdienst/alarm?assignment=${day.assignmentId}`}
>
Notdienstalarm auslösen
</Link>
) : null}
</div>
<div className="italic text-[var(--ink-faint)]">Noch offen</div>
<div>
<Badge
variant={
day.isInvalid
? "warning"
: assignedChild
? "success"
: "destructive"
}
>
{day.isInvalid
? "Bitte korrigieren"
: assignedChild
? "Besetzt"
: "Offen"}
</Badge>
</div>
</div>
);
})}
</div>
</Card>
);
}
export function ManualAssignmentTable({
days,
allChildren,
familyStatuses,
isDraft,
isPending,
onAssignmentChange,
}: Pick<PlanViewProps, "days" | "allChildren" | "familyStatuses"> & {
isDraft: boolean;
isPending: boolean;
onAssignmentChange: (date: string, newChildId: string) => void;
}) {
const availabilityByFamily = useMemo(
() =>
new Map(
familyStatuses.map((family) => [
family.id,
new Set(family.selectedDays.map((day) => day.iso.slice(0, 10))),
]),
),
[familyStatuses],
);
// Map childId to familyId for quick lookup
const childFamilyMap = useMemo(() => {
const map = new Map<string, string>();
allChildren.forEach((child) => {
if (child.familyId) {
map.set(child.id, child.familyId);
}
});
return map;
}, [allChildren]);
// Compute family assignment counts in real-time based on current selections
const familyAssignmentCounts = useMemo(() => {
const counts = new Map<string, number>();
days.forEach((day) => {
if (day.childId) {
const famId = childFamilyMap.get(day.childId);
if (famId) {
counts.set(famId, (counts.get(famId) ?? 0) + 1);
}
}
});
return counts;
}, [days, childFamilyMap]);
return (
<Card className="overflow-hidden p-0">
<div className="border-b border-[var(--hairline-soft)] p-4 text-sm leading-6 text-[var(--ink-soft)]">
Verfügbare Familien stehen zuerst. Familien ohne Verfügbarkeit für den
Tag bleiben für abgestimmte Sonderfälle auswählbar. Die Einteilungszahl
gilt für den gesamten Haushalt.
</div>
<div className="hidden sm:grid grid-cols-[1fr_2fr] border-b border-[var(--hairline-soft)] bg-[var(--surface-2)] p-4 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
<div>Datum</div>
<div>Eingeteilte Familie</div>
</div>
<div className="divide-y divide-[var(--hairline-soft)]">
{days.map((day) => {
const [yr, mo, dy] = day.date.slice(0, 10).split("-").map(Number);
const dateObj = new Date(yr, mo - 1, dy);
const dateKey = day.date.slice(0, 10);
const available = allChildren.filter((child) =>
availabilityByFamily.get(child.familyId)?.has(dateKey),
);
const unavailable = allChildren.filter(
(child) => !availabilityByFamily.get(child.familyId)?.has(dateKey),
);
const availableFamilies = new Set(
available.map((child) => child.familyId),
).size;
const selectedChild = allChildren.find(
(child) => child.id === day.childId,
);
const selectedAvailable =
!!selectedChild &&
availabilityByFamily.get(selectedChild.familyId)?.has(dateKey);
const option = (child: PlanViewProps["allChildren"][number]) => (
<option key={child.id} value={child.id} disabled={day.isInvalid}>
{child.parentName} ({child.name}) —{" "}
{familyAssignmentCounts.get(child.familyId) ?? 0}x
</option>
);
return (
<div
key={day.date}
className="grid gap-3 sm:grid-cols-[1fr_2fr] items-center p-4 transition-colors hover:bg-[var(--surface-2)]"
>
<div className="font-medium text-[var(--ink)]">
{format(dateObj, "EE, dd.MM.", { locale: de })}
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="w-full max-w-sm space-y-2">
<select
aria-label={`Familie für ${format(dateObj, "dd.MM.yyyy")}`}
className="w-full max-w-sm rounded-lg border border-[var(--hairline)] bg-[var(--surface)] px-3 py-1.5 text-sm text-[var(--ink)] ring-offset-[var(--surface)] hover:border-[var(--accent-mid)] focus-visible:border-[var(--accent)] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)]"
value={day.childId || "empty"}
disabled={isPending || !!day.activeAlarmId}
onChange={(event) =>
onAssignmentChange(day.date, event.target.value)
}
>
<option value="empty">-- Unbesetzt --</option>
{day.childId &&
!allChildren.some((child) => child.id === day.childId) ? (
<option value={day.childId} disabled>
Inaktives Kind – bitte Einteilung entfernen
</option>
) : null}
{available.length > 0 ? (
<optgroup label="Verfügbar an diesem Tag">
{available.map(option)}
</optgroup>
) : null}
{unavailable.length > 0 ? (
<optgroup label="Keine Verfügbarkeit für diesen Tag">
{unavailable.map(option)}
</optgroup>
) : null}
</select>
{!day.isInvalid ? (
<p
className={`text-xs leading-5 ${(selectedChild && !selectedAvailable) || availableFamilies === 0 ? "text-[var(--warn)]" : "text-[var(--ink-soft)]"}`}
>
{selectedChild
? selectedAvailable
? "Eingeteilte Familie ist verfügbar."
: "Eingeteilte Familie hat für diesen Tag keine Verfügbarkeit angegeben."
: `${availableFamilies} ${availableFamilies === 1 ? "Familie ist" : "Familien sind"} verfügbar.`}
</p>
) : null}
</div>
{day.isInvalid ? (
<span className="text-xs text-[var(--danger)]">
Einteilung nicht mehr gültig – bitte entfernen.
</span>
) : null}
{!isDraft && (
<span className="text-[11px] font-semibold text-[oklch(0.60_0.18_45)] bg-[oklch(0.95_0.02_45)] border border-[oklch(0.85_0.05_45)] px-2 py-0.5 rounded-md shrink-0">
Veröffentlicht
</span>
)}
{day.assignmentId && day.canAlarm && (
<Link
className="text-xs font-semibold text-[var(--accent-deep)] underline underline-offset-4"
href={
day.activeAlarmId
? `/dashboard/notdienst/alarm?alarm=${day.activeAlarmId}`
: `/dashboard/notdienst/alarm?assignment=${day.assignmentId}`
}
>
{day.activeAlarmId
? "Aktiven Alarm öffnen"
: "Notdienstalarm auslösen"}
</Link>
)}
</div>
</div>
);
})}
</div>
</Card>
);
}