feat: implement emergency duty (Notdienst) swapping for parents
This commit is contained in:
@@ -4,8 +4,6 @@ import { useState, useMemo, useTransition } from "react";
|
||||
import {
|
||||
TerminType,
|
||||
TerminStatus,
|
||||
EventParticipationStatus,
|
||||
DutyAssignmentStatus,
|
||||
} from "@prisma/client";
|
||||
import {
|
||||
format,
|
||||
@@ -30,9 +28,7 @@ import {
|
||||
MapPin,
|
||||
AlertTriangle,
|
||||
WashingMachine,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
Edit,
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -49,11 +45,20 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { CalendarListItemDto } from "./termin-list";
|
||||
import { TerminList } from "./termin-list";
|
||||
import {
|
||||
CalendarListItemDto,
|
||||
NotdienstSwapDialog,
|
||||
NotdienstSwapOptionDto,
|
||||
TerminList,
|
||||
} from "./termin-list";
|
||||
import { MitbringselList } from "./mitbringsel-list";
|
||||
import { AdminTerminModal } from "./admin-termin-modal";
|
||||
import { deleteTerminAdmin } from "../actions";
|
||||
import {
|
||||
acceptNotdienstSwapRequest,
|
||||
cancelNotdienstSwapRequest,
|
||||
declineNotdienstSwapRequest,
|
||||
deleteTerminAdmin,
|
||||
} from "../actions";
|
||||
|
||||
const categories = [
|
||||
{ label: "Schließtag", type: TerminType.SCHLIESSTAG, color: "var(--danger)" },
|
||||
@@ -68,14 +73,30 @@ const categories = [
|
||||
{ label: "Sonstiges", type: TerminType.SONSTIGES, color: "var(--ink-muted)" },
|
||||
];
|
||||
|
||||
export type NotdienstSwapRequestDto = {
|
||||
id: string;
|
||||
direction: "incoming" | "outgoing";
|
||||
targetDate: Date;
|
||||
targetFamilyName: string;
|
||||
targetChildName: string;
|
||||
offeredDate: Date;
|
||||
offeredFamilyName: string;
|
||||
offeredChildName: string;
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
export function CalendarOverviewClient({
|
||||
termine,
|
||||
userId,
|
||||
isAdmin,
|
||||
notdienstSwapOptions = [],
|
||||
pendingSwapRequests = [],
|
||||
}: {
|
||||
termine: CalendarListItemDto[];
|
||||
userId: string;
|
||||
isAdmin: boolean;
|
||||
notdienstSwapOptions?: NotdienstSwapOptionDto[];
|
||||
pendingSwapRequests?: NotdienstSwapRequestDto[];
|
||||
}) {
|
||||
const [view, setView] = useState<"liste" | "monat" | "agenda">("monat");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -185,6 +206,10 @@ export function CalendarOverviewClient({
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{!isAdmin && pendingSwapRequests.length > 0 && (
|
||||
<NotdienstSwapRequestsPanel requests={pendingSwapRequests} />
|
||||
)}
|
||||
|
||||
{/* Search and Filters Header */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-wrap gap-3">
|
||||
@@ -282,7 +307,12 @@ export function CalendarOverviewClient({
|
||||
|
||||
{/* Main View Contents */}
|
||||
{view === "liste" && (
|
||||
<TerminList termine={filteredEvents} userId={userId} isAdmin={isAdmin} />
|
||||
<TerminList
|
||||
termine={filteredEvents}
|
||||
userId={userId}
|
||||
isAdmin={isAdmin}
|
||||
notdienstSwapOptions={notdienstSwapOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === "monat" && (
|
||||
@@ -431,13 +461,26 @@ export function CalendarOverviewClient({
|
||||
</p>
|
||||
{isAdmin && (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<a href="/dashboard/notdienst/plan">
|
||||
<a href={`/dashboard/notdienst/plan?year=${item.planYear}&month=${item.planMonth}`}>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs px-2.5">
|
||||
Im Planer bearbeiten
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!isAdmin && item.canRequestSwap && (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<NotdienstSwapDialog
|
||||
target={item}
|
||||
options={notdienstSwapOptions}
|
||||
trigger={
|
||||
<Button size="sm" variant="outline" className="h-7 px-2.5 text-xs">
|
||||
Tausch anfragen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -564,6 +607,106 @@ export function CalendarOverviewClient({
|
||||
);
|
||||
}
|
||||
|
||||
function NotdienstSwapRequestsPanel({
|
||||
requests,
|
||||
}: {
|
||||
requests: NotdienstSwapRequestDto[];
|
||||
}) {
|
||||
return (
|
||||
<Card className="p-0">
|
||||
<div className="border-b border-[var(--hairline-soft)] px-5 py-4">
|
||||
<h2 className="text-base font-semibold tracking-[-0.01em] text-[var(--ink)]">
|
||||
Offene Notdienst-Tausche
|
||||
</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-[var(--hairline-soft)]">
|
||||
{requests.map((request) => (
|
||||
<NotdienstSwapRequestRow key={request.id} request={request} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function NotdienstSwapRequestRow({
|
||||
request,
|
||||
}: {
|
||||
request: NotdienstSwapRequestDto;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const runAction = (
|
||||
action: (requestId: string) => Promise<{ success?: boolean; error?: string }>,
|
||||
successMessage: string,
|
||||
) => {
|
||||
startTransition(async () => {
|
||||
const result = await action(request.id);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
toast.success(successMessage);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-[var(--ink)]">
|
||||
{request.direction === "incoming" ? "Anfrage an dich" : "Deine Anfrage"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-5 text-[var(--ink-soft)]">
|
||||
{format(request.targetDate, "dd.MM.yyyy", { locale: de })}{" "}
|
||||
({request.targetFamilyName}, {request.targetChildName}) gegen{" "}
|
||||
{format(request.offeredDate, "dd.MM.yyyy", { locale: de })}{" "}
|
||||
({request.offeredFamilyName}, {request.offeredChildName})
|
||||
</p>
|
||||
{request.message && (
|
||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
{request.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
{request.direction === "incoming" ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
runAction(acceptNotdienstSwapRequest, "Tausch wurde angenommen.")
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annehmen
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
runAction(declineNotdienstSwapRequest, "Tausch wurde abgelehnt.")
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
Ablehnen
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
runAction(cancelNotdienstSwapRequest, "Tauschanfrage wurde zurückgezogen.")
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
Zurückziehen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Month Grid Component
|
||||
// =====================================================================
|
||||
|
||||
Reference in New Issue
Block a user