Files
kita-planer/src/app/dashboard/admin/dienste/duty-planner.tsx
T

217 lines
6.3 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { BellRing, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
checkAndSendDutyReminders,
upsertDutyAssignment,
} from "./actions";
const UNASSIGNED = "__unassigned__";
export type DutyPlanWeekDto = {
key: string;
label: string;
startDate: string;
endDate: string;
};
export type DutyPlanDutyTypeDto = {
id: string;
name: string;
description: string | null;
};
export type DutyPlanFamilyDto = {
id: string;
name: string;
};
export type DutyPlanAssignmentDto = {
id: string;
dutyTypeId: string;
familyId: string;
startDate: string;
};
export function DutyPlanner({
weeks,
dutyTypes,
families,
assignments,
}: {
weeks: DutyPlanWeekDto[];
dutyTypes: DutyPlanDutyTypeDto[];
families: DutyPlanFamilyDto[];
assignments: DutyPlanAssignmentDto[];
}) {
const [isPending, startTransition] = useTransition();
const [pendingCell, setPendingCell] = useState<string | null>(null);
const assignmentByCell = new Map(
assignments.map((assignment) => [
`${assignment.startDate}:${assignment.dutyTypeId}`,
assignment,
]),
);
function handleAssignmentChange(
week: DutyPlanWeekDto,
dutyTypeId: string,
value: string,
) {
const cellKey = `${week.startDate}:${dutyTypeId}`;
setPendingCell(cellKey);
startTransition(async () => {
const result = await upsertDutyAssignment({
dutyTypeId,
familyId: value === UNASSIGNED ? null : value,
startDate: week.startDate,
endDate: week.endDate,
});
if (result.error) {
toast.error(result.error);
} else {
toast.success(
value === UNASSIGNED
? "Zuweisung entfernt"
: "Dienstplan aktualisiert",
);
}
setPendingCell(null);
});
}
function sendReminders() {
startTransition(async () => {
const result = await checkAndSendDutyReminders();
if (result.error) {
toast.error(result.error);
return;
}
toast.success(
`Erinnerungen versendet: ${result.sent ?? 0}. Ohne Empfaenger: ${
result.skipped ?? 0
}.`,
);
if (result.errors?.length) {
toast.warning(`${result.errors.length} Erinnerung(en) fehlgeschlagen.`);
}
});
}
if (dutyTypes.length === 0) {
return (
<div className="rounded-lg border border-dashed border-[var(--hairline)] bg-[var(--surface-2)] p-8 text-center">
<p className="text-sm font-medium text-[var(--ink)]">
Noch keine Diensttypen
</p>
<p className="mt-1 text-sm text-[var(--ink-soft)]">
Lege zuerst einen Diensttyp an, danach erscheint hier das Planungsgrid.
</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Button
type="button"
variant="outline"
size="sm"
onClick={sendReminders}
disabled={isPending}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin [stroke-width:1.7]" />
) : (
<BellRing className="h-4 w-4 [stroke-width:1.7]" />
)}
Reminder diese Woche senden
</Button>
</div>
<div className="overflow-x-auto rounded-lg border border-[var(--hairline)]">
<table className="w-full min-w-[760px] border-collapse text-[13.5px] text-[var(--ink)]">
<thead>
<tr className="border-b border-[var(--hairline-soft)] bg-[var(--surface-2)]">
<th className="sticky left-0 z-10 w-56 bg-[var(--surface-2)] px-4 py-3 text-left text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Woche
</th>
{dutyTypes.map((dutyType) => (
<th
key={dutyType.id}
className="min-w-56 px-4 py-3 text-left font-medium text-[var(--ink)]"
>
<div>{dutyType.name}</div>
{dutyType.description && (
<div className="mt-1 line-clamp-1 text-xs font-normal text-[var(--ink-muted)]">
{dutyType.description}
</div>
)}
</th>
))}
</tr>
</thead>
<tbody>
{weeks.map((week) => (
<tr
key={week.key}
className="border-b border-[var(--hairline-soft)] last:border-b-0 hover:bg-[var(--surface-2)]"
>
<td className="sticky left-0 z-10 bg-[var(--surface)] px-4 py-3 font-medium">
{week.label}
</td>
{dutyTypes.map((dutyType) => {
const cellKey = `${week.startDate}:${dutyType.id}`;
const currentAssignment = assignmentByCell.get(cellKey);
return (
<td key={cellKey} className="px-4 py-3">
<Select
value={currentAssignment?.familyId ?? UNASSIGNED}
onValueChange={(value) =>
handleAssignmentChange(week, dutyType.id, value)
}
disabled={isPending && pendingCell === cellKey}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Familie wählen" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNASSIGNED}>
Nicht zugewiesen
</SelectItem>
{families.map((family) => (
<SelectItem key={family.id} value={family.id}>
{family.name}
</SelectItem>
))}
</SelectContent>
</Select>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}