Implement missing UI button features, filters, search, and view toggles across all modules

This commit is contained in:
t.indorf
2026-05-21 10:44:23 +02:00
parent e7eb43462c
commit 84aae1d72e
18 changed files with 3268 additions and 958 deletions
+90 -8
View File
@@ -18,10 +18,20 @@ 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 {
@@ -54,6 +64,8 @@ interface PlanViewProps {
coverageFactor: number;
closureCount: number;
monthName: string;
year: number;
month: number;
}
export function PlanView({
@@ -70,16 +82,19 @@ export function PlanView({
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();
const result = await generatePlanAction(year, month);
if ("error" in result && result.error) {
toast.error(result.error as string);
} else {
@@ -100,6 +115,30 @@ export function PlanView({
});
};
const handleSendReminders = () => {
startTransition(async () => {
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.`);
}
});
};
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") return;
const valueToSet = newChildId === "empty" ? null : newChildId;
@@ -132,8 +171,12 @@ export function PlanView({
<Button disabled={isPending} onClick={handleGenerate}>
Trotzdem generieren
</Button>
<Button variant="ghost">
<Send className="h-4 w-4 [stroke-width:1.7]" />
<Button variant="ghost" onClick={handleSendReminders} disabled={isPending}>
{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>
@@ -199,8 +242,16 @@ export function PlanView({
</div>
<div className="border-t border-[var(--hairline-soft)] px-6 py-4">
<Button variant="ghost" disabled={openFamilies === 0}>
<Send className="h-4 w-4 [stroke-width:1.7]" />
<Button
variant="ghost"
onClick={handleSendReminders}
disabled={isPending || openFamilies === 0}
>
{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>
@@ -282,7 +333,7 @@ export function PlanView({
return (
<div className="space-y-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<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"}
@@ -309,13 +360,15 @@ export function PlanView({
Plan veröffentlichen
</Button>
)}
<Button variant="ghost">
<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
@@ -323,7 +376,7 @@ export function PlanView({
</div>
</div>
<Card className="flex items-center justify-between p-4">
<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"}
@@ -347,6 +400,35 @@ export function PlanView({
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>
);
}