"use client"; import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { AlertCircle, X } from "lucide-react"; import type { PlanningConflict } from "@/lib/planning/capacity"; import { formatMinutes } from "@/lib/planning/text"; import { wallTimeToUtc } from "@/lib/work-orders/time"; import { buttonCls } from "@/components/work-orders/button-cls"; import { Field, inputCls } from "@/components/work-orders/ui"; import type { BoardOrder } from "@/server/services/planning/data"; export type DialogState = { order: BoardOrder; teamId: string; day: string; time: string; duration: number }; export type ScheduleResponse = { id: string; number: string; version: number; status: string; teamId: string; plannedStart: string; plannedEnd: string | null; plannedDurationMinutes: number | null; conflicts: PlanningConflict[]; }; /** * Confirmation popover for drag & drop and the keyboard path ("Einplanen …"): team, date, start and * duration are prefilled; nothing is saved without "Speichern". Escape / backdrop / × close it and focus * returns to the element that opened it. */ export function ScheduleDialog({ state, teams, timeZone, locale, onClose, onSaved, onConflict, }: { state: DialogState; teams: { id: string; name: string }[]; timeZone: string; locale: string; onClose: () => void; onSaved: (res: ScheduleResponse) => void; onConflict: (message: string) => void; }) { const t = useTranslations("planning"); const [teamId, setTeamId] = useState(state.teamId); const [day, setDay] = useState(state.day); const [time, setTime] = useState(state.time || "08:00"); const [allDay, setAllDay] = useState(state.time === ""); const [duration, setDuration] = useState(state.duration); const [pending, setPending] = useState(false); const [error, setError] = useState(null); const closeRef = useRef(onClose); useEffect(() => { closeRef.current = onClose; }, [onClose]); useEffect(() => { const opener = document.activeElement as HTMLElement | null; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") closeRef.current(); }; window.addEventListener("keydown", onKey); return () => { window.removeEventListener("keydown", onKey); opener?.focus?.(); }; }, []); const order = state.order; async function submit(e: React.FormEvent) { e.preventDefault(); setError(null); const start = wallTimeToUtc(allDay ? day : `${day}T${time}`, timeZone); if (!start || !teamId || !Number.isFinite(duration) || duration < 15) { setError(t("errors.invalid")); return; } const body: Record = { workOrderId: order.id, teamId, plannedStart: start.toISOString(), baseVersion: order.version }; if (!order.multiDay) { body.plannedDurationMinutes = Math.round(duration); body.plannedEnd = allDay ? null : new Date(start.getTime() + Math.round(duration) * 60_000).toISOString(); } setPending(true); try { const res = await fetch("/api/v1/planning/schedule", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const json = (await res.json().catch(() => null)) as (ScheduleResponse & { error?: { code?: string; message?: string } }) | null; if (res.ok && json) { onSaved(json); return; } if (res.status === 409) { onConflict(t("errors.conflict")); return; } const message = json?.error?.message ?? ""; const code = json?.error?.code ?? ""; setError(t.has(`errors.${message}`) ? t(`errors.${message}`) : t.has(`errors.${code}`) ? t(`errors.${code}`) : t("errors.generic")); } catch { setError(t("errors.network")); } finally { setPending(false); } } return (
{ if (e.target === e.currentTarget) onClose(); }} >

{t("dialog.title")}

{order.number} · {order.customerName}

{(order.siteAddress || order.siteName) &&

{order.siteAddress ?? order.siteName}

}
setDay(e.target.value)} required className={inputCls} /> setTime(e.target.value)} disabled={allDay} required={!allDay} className={inputCls} /> {!order.multiDay && ( setDuration(Number(e.target.value))} required className={inputCls} /> )} {error && (

{error}

)}
); }