"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
);
}