feat: add household emergency duty alarms and deadline worker

This commit is contained in:
t.indorf
2026-10-09 11:02:24 +02:00
parent a96ba0fd84
commit 8b32e4a832
34 changed files with 3947 additions and 646 deletions
+49 -6
View File
@@ -1,6 +1,7 @@
"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";
@@ -40,6 +41,8 @@ interface PlanViewProps {
days: {
date: string;
assignmentId: string | null;
activeAlarmId?: string | null;
canAlarm?: boolean;
childId: string | null;
isOriginallyUnassigned?: boolean;
}[];
@@ -92,7 +95,9 @@ export function PlanView({
const [resetDialogOpen, setResetDialogOpen] = useState(false);
const responsePercent =
totalFamilies > 0 ? Math.round((respondedFamilies / totalFamilies) * 100) : 0;
totalFamilies > 0
? Math.round((respondedFamilies / totalFamilies) * 100)
: 0;
const handleGenerate = () => {
startTransition(async () => {
@@ -123,7 +128,9 @@ export function PlanView({
if ("error" in result && result.error) {
toast.error(result.error as string);
} else if (result.success) {
toast.success(`${result.count} Erinnerungs-E-Mail(s) erfolgreich versendet.`);
toast.success(
`${result.count} Erinnerungs-E-Mail(s) erfolgreich versendet.`,
);
}
});
};
@@ -173,7 +180,11 @@ export function PlanView({
<Button disabled={isPending} onClick={handleGenerate}>
Trotzdem generieren
</Button>
<Button variant="ghost" onClick={handleSendReminders} disabled={isPending}>
<Button
variant="ghost"
onClick={handleSendReminders}
disabled={isPending}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
) : (
@@ -408,7 +419,8 @@ export function PlanView({
<DialogHeader>
<DialogTitle>Plan zurücksetzen?</DialogTitle>
<DialogDescription>
Möchtest du den Plan wirklich zurücksetzen? Alle Zuteilungen für diesen Monat gehen unwiderruflich verloren.
Möchtest du den Plan wirklich zurücksetzen? Alle Zuteilungen für
diesen Monat gehen unwiderruflich verloren.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -542,6 +554,21 @@ function PlanPreviewTable({
({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>
@@ -616,7 +643,7 @@ function ManualAssignmentTable({
<select
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}
disabled={isPending || !!day.activeAlarmId}
onChange={(event) =>
onAssignmentChange(day.date, event.target.value)
}
@@ -624,7 +651,9 @@ function ManualAssignmentTable({
<option value="empty">-- Unbesetzt --</option>
{allChildren.map((child) => {
const famId = child.familyId;
const count = famId ? (familyAssignmentCounts.get(famId) ?? 0) : 0;
const count = famId
? (familyAssignmentCounts.get(famId) ?? 0)
: 0;
return (
<option key={child.id} value={child.id}>
{child.parentName} ({child.name}) — {count}x
@@ -637,6 +666,20 @@ function ManualAssignmentTable({
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>
);