"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { ArrowLeft, CheckCircle2, Copy, Edit, Link2, Mail, Send, ShieldCheck, Wrench, XCircle } from "lucide-react"; import DetailSection from "@/components/common/DetailSection"; import { useToast } from "@/components/common/ToastProvider"; import RepairFormDialog from "@/components/repairs/RepairFormDialog"; import { RepairPriorityBadge, RepairStatusBadge, statusLabels } from "@/components/repairs/RepairStatusBadge"; import RepairStatusDialog from "@/components/repairs/RepairStatusDialog"; import { Button, buttonVariants } from "@/components/ui/button"; import { api } from "@/lib/api"; import { hasPermission } from "@/lib/permissions"; import type { CurrentUser } from "@/types/rbac"; import type { Repair, RepairNotificationOverview, RepairPayload, RepairPublicLink, RepairPublicLinkCreated, RepairStatus, RepairStatusHistory, } from "@/types/repair"; type Params = { params: Promise<{ id: string; }>; }; function getErrorMessage(error: unknown) { if (typeof error === "object" && error !== null && "response" in error) { const response = (error as { response?: { data?: { detail?: string; message?: string } } }).response; return response?.data?.detail ?? response?.data?.message ?? "Aktion konnte nicht abgeschlossen werden"; } return "Aktion konnte nicht abgeschlossen werden"; } function formatDate(value?: string | null) { if (!value) return "-"; return new Intl.DateTimeFormat("de-DE", { dateStyle: "short", timeStyle: "short" }).format(new Date(value)); } function DetailItem({ label, value }: { label: string; value?: string | number | boolean | null }) { return (
{label}
{typeof value === "boolean" ? (value ? "Ja" : "Nein") : value || "-"}
); } function historyLabel(item: RepairStatusHistory) { const oldStatus = item.old_status ? statusLabels[item.old_status as RepairStatus] ?? item.old_status : "Start"; return `${oldStatus} → ${statusLabels[item.new_status]}`; } export default function RepairDetailPage({ params }: Params) { const { showToast } = useToast(); const [repairId, setRepairId] = useState(""); const [repair, setRepair] = useState(null); const [history, setHistory] = useState([]); const [publicLink, setPublicLink] = useState(null); const [lastCreatedStatusPath, setLastCreatedStatusPath] = useState(""); const [notifications, setNotifications] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [pending, setPending] = useState(false); const [formOpen, setFormOpen] = useState(false); const [statusOpen, setStatusOpen] = useState(false); const [formError, setFormError] = useState(""); const [statusError, setStatusError] = useState(""); const [communicationPending, setCommunicationPending] = useState(false); useEffect(() => { params.then((resolved) => setRepairId(resolved.id)); }, [params]); const loadRepair = useCallback(async () => { if (!repairId) return; setLoading(true); setError(""); try { const [repairResponse, historyResponse, meResponse] = await Promise.all([ api.get(`/repairs/${repairId}`), api.get(`/repairs/${repairId}/history`), api.get("/me"), ]); setRepair(repairResponse.data); setHistory(historyResponse.data); setCurrentUser(meResponse.data); const notificationResponse = await api.get(`/repairs/${repairId}/notifications`); setNotifications(notificationResponse.data); if (hasPermission(meResponse.data, "repairs.public_link.manage")) { const publicLinkResponse = await api.get(`/repairs/${repairId}/public-link`); setPublicLink(publicLinkResponse.data); } else { setPublicLink(null); } setLastCreatedStatusPath(""); } catch (err) { setError(getErrorMessage(err)); } finally { setLoading(false); } }, [repairId]); useEffect(() => { queueMicrotask(() => { void loadRepair(); }); }, [loadRepair]); async function saveRepair(payload: RepairPayload) { if (!repair) return; setPending(true); setFormError(""); try { const response = await api.put(`/repairs/${repair.id}`, payload); setRepair(response.data); setFormOpen(false); showToast({ type: "success", title: "Reparatur aktualisiert", description: response.data.repair_number }); } catch (err) { const message = getErrorMessage(err); setFormError(message); showToast({ type: "error", title: "Reparatur konnte nicht gespeichert werden", description: message }); } finally { setPending(false); } } async function saveStatus(status: RepairStatus, note: string) { if (!repair) return; setPending(true); setStatusError(""); try { const response = await api.put(`/repairs/${repair.id}/status`, { status, note }); const [historyResponse, notificationResponse, publicLinkResponse] = await Promise.all([ api.get(`/repairs/${repair.id}/history`), api.get(`/repairs/${repair.id}/notifications`), canManagePublicLink ? api.get(`/repairs/${repair.id}/public-link`) : Promise.resolve(null), ]); setRepair(response.data); setHistory(historyResponse.data); setNotifications(notificationResponse.data); if (publicLinkResponse) setPublicLink(publicLinkResponse.data); setStatusOpen(false); showToast({ type: "success", title: "Status aktualisiert", description: response.data.repair_number }); } catch (err) { const message = getErrorMessage(err); setStatusError(message); showToast({ type: "error", title: "Status konnte nicht gespeichert werden", description: message }); } finally { setPending(false); } } if (loading) { return
Reparatur wird geladen...
; } if (error || !repair) { return
{error || "Reparatur nicht gefunden"}
; } const canUpdate = hasPermission(currentUser, "repairs.update"); const canUpdateStatus = hasPermission(currentUser, "repairs.status.update"); const canManagePublicLink = hasPermission(currentUser, "repairs.public_link.manage"); async function createPublicLink() { if (!repair || !canManagePublicLink) return; setCommunicationPending(true); try { const response = await api.post(`/repairs/${repair.id}/public-link`); setPublicLink(response.data); setLastCreatedStatusPath(response.data.public_status_path); showToast({ type: "success", title: "Statuslink erstellt", description: "Der Klartextlink wird nur jetzt angezeigt." }); } catch (err) { const message = getErrorMessage(err); showToast({ type: "error", title: "Statuslink konnte nicht erstellt werden", description: message }); } finally { setCommunicationPending(false); } } async function revokePublicLink() { if (!repair || !canManagePublicLink) return; setCommunicationPending(true); try { const response = await api.delete(`/repairs/${repair.id}/public-link`); setPublicLink(response.data); setLastCreatedStatusPath(""); showToast({ type: "success", title: "Statuslink deaktiviert", description: repair.repair_number }); } catch (err) { const message = getErrorMessage(err); showToast({ type: "error", title: "Statuslink konnte nicht deaktiviert werden", description: message }); } finally { setCommunicationPending(false); } } async function sendStatusMail() { if (!repair || !canUpdate) return; setCommunicationPending(true); try { await api.post(`/repairs/${repair.id}/send-status-mail`); const [notificationResponse, publicLinkResponse] = await Promise.all([ api.get(`/repairs/${repair.id}/notifications`), canManagePublicLink ? api.get(`/repairs/${repair.id}/public-link`) : Promise.resolve(null), ]); setNotifications(notificationResponse.data); if (publicLinkResponse) setPublicLink(publicLinkResponse.data); setLastCreatedStatusPath(""); showToast({ type: "success", title: "Statusmail verarbeitet", description: "Der Versandversuch wurde dokumentiert." }); } catch (err) { const message = getErrorMessage(err); showToast({ type: "error", title: "Statusmail konnte nicht verarbeitet werden", description: message }); } finally { setCommunicationPending(false); } } async function copyPublicLink() { if (!lastCreatedStatusPath) { showToast({ type: "error", title: "Kein Klartextlink verfügbar", description: "Der sichere Link wird nur direkt nach der Erstellung angezeigt." }); return; } const absolutePath = new URL(lastCreatedStatusPath, window.location.origin).toString(); try { await navigator.clipboard.writeText(absolutePath); showToast({ type: "success", title: "Link kopiert", description: lastCreatedStatusPath }); } catch { showToast({ type: "error", title: "Link konnte nicht kopiert werden", description: "Bitte kopiere den angezeigten Link manuell." }); } } return (
Zurück

{repair.repair_number}

{repair.customer_name} · {repair.device_manufacturer} {repair.device_model}

{canUpdate && } {canUpdateStatus && }
{history.length === 0 ? (

Noch keine Statushistorie vorhanden.

) : (
{history.map((item) => (

{historyLabel(item)}

{item.new_status === repair.status && Aktuell}

{item.actor_display_name || item.actor_username || "System"}

{item.note &&

{item.note}

}
))}
)}
{(() => { const lastMail = notifications?.events[0] ?? null; return (
{publicLink?.is_active ? : }

Öffentlicher Statuslink {publicLink?.is_active ? "aktiv" : "inaktiv"}

Kunden erhalten später Statusupdates per E-Mail mit sicherem Link. Ein Kundenlogin ist dafür nicht erforderlich.

{lastCreatedStatusPath && (

Einmaliger Klartextlink

{lastCreatedStatusPath}

)}
{canUpdate && } {canManagePublicLink && ( <> )}
{!canManagePublicLink && (

Für die Verwaltung des Statuslinks ist `repairs.public_link.manage` erforderlich.

)}
Benachrichtigungen

Versandhistorie

{notifications?.events.length ? (
{notifications.events.slice(0, 5).map((event) => (

{event.subject}

{event.success ? "Erfolgreich" : event.status}

{formatDate(event.created_at)} · {event.recipient || "Kein Empfänger"}

{event.error_message &&

{event.error_message}

}
))}
) : (

Noch kein Versandversuch dokumentiert.

)}
{notifications?.templates.map((template) => (

{template.subject}

{template.status && }

{template.text}

))} {!notifications?.templates.length &&

Noch keine Vorlagen vorbereitet.

}

Statusmails werden beim Statuswechsel automatisch verarbeitet. Interne Notizen werden nicht öffentlich ausgegeben.

); })()}

Reparaturaktionen werden in den Audit Logs erfasst und erscheinen im Activity Feed, wenn `repairs.read` vorhanden ist.

Audit Logs öffnen
); }