feat: complete notdienst planning, quick wins and notifications
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
|
||||
interface PlanViewProps {
|
||||
planId?: string;
|
||||
remindersOpen: boolean;
|
||||
status?: NotdienstPlanStatus;
|
||||
days: {
|
||||
date: string;
|
||||
@@ -45,6 +46,7 @@ interface PlanViewProps {
|
||||
canAlarm?: boolean;
|
||||
childId: string | null;
|
||||
isOriginallyUnassigned?: boolean;
|
||||
isInvalid?: boolean;
|
||||
}[];
|
||||
allChildren: {
|
||||
id: string;
|
||||
@@ -56,6 +58,7 @@ interface PlanViewProps {
|
||||
id: string;
|
||||
name: string;
|
||||
childCount: number;
|
||||
requiredDays: number;
|
||||
selectedDays: {
|
||||
iso: string;
|
||||
label: string;
|
||||
@@ -75,6 +78,7 @@ interface PlanViewProps {
|
||||
|
||||
export function PlanView({
|
||||
planId,
|
||||
remindersOpen,
|
||||
status,
|
||||
days,
|
||||
allChildren,
|
||||
@@ -116,8 +120,9 @@ export function PlanView({
|
||||
const result = await publishPlanAction(planId);
|
||||
if ("error" in result && result.error) {
|
||||
toast.error(result.error as string);
|
||||
} else {
|
||||
toast.success("Plan veröffentlicht! Er ist nun fix.");
|
||||
} 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.");
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -127,10 +132,9 @@ export function PlanView({
|
||||
const result = await sendNotdienstRemindersAction(year, month);
|
||||
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.`,
|
||||
);
|
||||
} 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.`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -153,7 +157,7 @@ export function PlanView({
|
||||
const valueToSet = newChildId === "empty" ? null : newChildId;
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await updateAssignmentAction(planId, date, valueToSet);
|
||||
const result = await updateAssignmentAction(planId, date.slice(0, 10), valueToSet);
|
||||
if ("error" in result && result.error) {
|
||||
toast.error(result.error as string);
|
||||
} else {
|
||||
@@ -162,7 +166,7 @@ export function PlanView({
|
||||
});
|
||||
};
|
||||
|
||||
if (!planId && respondedFamilies === 0) {
|
||||
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">
|
||||
@@ -183,7 +187,7 @@ export function PlanView({
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleSendReminders}
|
||||
disabled={isPending}
|
||||
disabled={isPending || !remindersOpen}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
|
||||
@@ -211,7 +215,7 @@ export function PlanView({
|
||||
</h2>
|
||||
<p className="mt-1 text-[13.5px] leading-5 text-[var(--ink-soft)]">
|
||||
{respondedFamilies} von {totalFamilies} Familien haben
|
||||
geantwortet · noch {openFamilies} offen
|
||||
vollständig geantwortet · noch {openFamilies} offen
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm font-semibold tabular-nums text-[var(--ink-soft)]">
|
||||
@@ -244,7 +248,7 @@ export function PlanView({
|
||||
{family.childCount === 1 ? "" : "er"}
|
||||
</p>
|
||||
</div>
|
||||
<FamilyStatusPill selectedDays={family.selectedDays.length} />
|
||||
<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(" · ")
|
||||
@@ -258,7 +262,7 @@ export function PlanView({
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleSendReminders}
|
||||
disabled={isPending || openFamilies === 0}
|
||||
disabled={isPending || openFamilies === 0 || !remindersOpen}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
|
||||
@@ -315,7 +319,7 @@ export function PlanView({
|
||||
</h3>
|
||||
<p className="mt-1 text-[13.5px] text-[var(--ink-soft)]">
|
||||
Verteilt {requiredDays} Notdiensttage fair auf{" "}
|
||||
{respondedFamilies} Familien.
|
||||
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)]">
|
||||
@@ -447,7 +451,7 @@ export function PlanView({
|
||||
);
|
||||
}
|
||||
|
||||
function FamilyStatusPill({ selectedDays }: { selectedDays: number }) {
|
||||
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)]">
|
||||
@@ -457,8 +461,8 @@ function FamilyStatusPill({ selectedDays }: { selectedDays: number }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="rounded-full border border-[var(--good)] bg-[var(--good-soft)] px-2.5 py-1 text-[11px] font-semibold text-[var(--good)]">
|
||||
{selectedDays} Tage
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -548,7 +552,7 @@ function PlanPreviewTable({
|
||||
{format(dateObj, "EEEE", { locale: de })}
|
||||
</div>
|
||||
<div className="text-[var(--ink)]">
|
||||
{assignedChild ? assignedChild.parentName : "Unbesetzt"}
|
||||
{assignedChild ? assignedChild.parentName : day.childId ? "Inaktives Kind" : "Unbesetzt"}
|
||||
{assignedChild && (
|
||||
<span className="ml-1 text-[var(--ink-muted)]">
|
||||
({assignedChild.name})
|
||||
@@ -572,8 +576,8 @@ function PlanPreviewTable({
|
||||
</div>
|
||||
<div className="italic text-[var(--ink-faint)]">Noch offen</div>
|
||||
<div>
|
||||
<Badge variant={assignedChild ? "success" : "destructive"}>
|
||||
{assignedChild ? "Besetzt" : "Offen"}
|
||||
<Badge variant={day.isInvalid ? "warning" : assignedChild ? "success" : "destructive"}>
|
||||
{day.isInvalid ? "Bitte korrigieren" : assignedChild ? "Besetzt" : "Offen"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -649,18 +653,24 @@ function ManualAssignmentTable({
|
||||
}
|
||||
>
|
||||
<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}
|
||||
{allChildren.map((child) => {
|
||||
const famId = child.familyId;
|
||||
const count = famId
|
||||
? (familyAssignmentCounts.get(famId) ?? 0)
|
||||
: 0;
|
||||
return (
|
||||
<option key={child.id} value={child.id}>
|
||||
<option key={child.id} value={child.id} disabled={day.isInvalid}>
|
||||
{child.parentName} ({child.name}) — {count}x
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{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
|
||||
|
||||
Reference in New Issue
Block a user