'use client'; import { useCallback, useEffect, useState, type FormEvent } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import { api, apiUpload, errMsg } from '@/lib/api'; import { useSession } from '@/lib/session'; import { Alert, Field, TicketStatusBadge, TICKET_PRIORITY, fmt } from '@/components/ui'; const MAX_FILES = 5; const ALLOWED = 'image/png,image/jpeg,image/gif,image/webp,application/pdf'; interface Attachment { id: string; filename: string; contentType: string; sizeBytes: number } function AttachmentList({ ticketId, atts }: { ticketId: string; atts: Attachment[] }) { if (atts.length === 0) return null; return (
{atts.map((a) => { const href = `/api/tickets/${ticketId}/attachments/${a.id}`; const kb = Math.max(1, Math.round(a.sizeBytes / 1024)); return a.contentType.startsWith('image/') ? {a.filename} : 📄 {a.filename} ({kb} KB); })}
); } interface Msg { id: string; authorId: string; authorName: string; authorKind: 'customer' | 'staff'; body: string; internalNote: boolean; createdAt: string; attachments: Attachment[] } interface Detail { id: string; number: string; orgId: string; orgName: string; subject: string; status: string; priority: string; assignedTo: string | null; assignedName: string | null; resourceId: string | null; resourceName: string | null; resourceType: string | null; domainRecordId: string | null; domain: string | null; messages: Msg[]; canWrite: boolean } interface StaffUser { id: string; name: string; staffRole: string | null } const STATUS_OPTIONS = { open: 'Offen', pending_customer: 'Wartet auf Kunde', pending_staff: 'Wartet auf Personal', resolved: 'Gelöst', closed: 'Geschlossen' }; export default function TicketDetail() { const { id } = useParams<{ id: string }>(); const { can } = useSession(); const staff = can('tickets.read'); const write = can('tickets.write'); const [t, setT] = useState(null); const [err, setErr] = useState(''); const [ok, setOk] = useState(''); const [busy, setBusy] = useState(false); const [staffUsers, setStaffUsers] = useState([]); const load = useCallback(() => api('GET', `/tickets/${id}`).then(setT).catch((e) => setErr(errMsg(e))), [id]); useEffect(() => { void load(); }, [load]); useEffect(() => { if (write) api('GET', '/admin/users').then((l) => setStaffUsers(l.filter((u) => !!u.staffRole))).catch(() => undefined); }, [write]); async function send(e: FormEvent) { e.preventDefault(); setErr(''); setBusy(true); const f = e.currentTarget; const fd = new FormData(f); const body = String(fd.get('body') ?? ''); const internal = fd.get('internal') === 'on'; const files = (fd.getAll('files') as File[]).filter((x) => x instanceof File && x.size > 0); try { const { messageId } = await api<{ messageId: string }>('POST', `/tickets/${id}/messages`, { body, internalNote: internal }); f.reset(); if (files.length > 0) { try { await apiUpload(`/tickets/${id}/messages/${messageId}/attachments`, files); } catch (x) { setErr(`Nachricht gesendet, aber Anhang fehlgeschlagen: ${errMsg(x)}`); } } void load(); } catch (x) { setErr(errMsg(x)); } finally { setBusy(false); } } async function patch(body: object, msg?: string) { setErr(''); setOk(''); try { await api('PATCH', `/tickets/${id}`, body); if (msg) setOk(msg); void load(); } catch (x) { setErr(errMsg(x)); } } async function close() { setErr(''); try { await api('POST', `/tickets/${id}/close`); void load(); } catch (x) { setErr(errMsg(x)); } } if (!t) return err ? {err} :

Wird geladen 


; const canClose = t.status !== 'closed'; return (<>

{t.number}: {t.subject}

← Tickets

{staff && <>{t.orgName} · } · PrioritÀt: {TICKET_PRIORITY[t.priority]}{t.assignedName && <> · zugewiesen an {t.assignedName}}

{(t.domain || t.resourceName) &&

Betrifft: {t.resourceId ? {t.resourceName} : staff ? {t.domain} : t.domain}

} {err && {err}}{ok && {ok}} {write &&
}
{t.messages.map((m) => (
{m.authorName} ({m.authorKind === 'staff' ? 'Personal' : 'Kunde'}){m.internalNote && <> · nur intern}{fmt(m.createdAt)}

{m.body}

))}
{t.status === 'closed' ?

Dieses Ticket ist geschlossen. FĂŒr neue Anliegen bitte ein neues Ticket eröffnen.

: t.canWrite &&