import { useMemo, useState } from 'react' import { Link, Navigate, useLocation, useNavigate, useParams } from 'react-router-dom' import { PageHeader, Tabs, Card, EmptyState, Button } from '../components/ui' import UrgentToggle from '../components/UrgentToggle' import WorksheetSummary from '../components/WorksheetSummary' import WorksheetList from '../components/WorksheetList' import StundenkontoLeiste from '../components/customer/StundenkontoLeiste' import ProjectContextPanel from '../components/ProjectContextPanel' import TicketProjects from '../components/TicketProjects' import CreateWorksheetModal from '../components/CreateWorksheetModal' import WorksheetTimesModal from '../components/WorksheetTimesModal' import OwnSupportModal from '../components/OwnSupportModal' import { useAuth } from '../context/AuthContext' import { useEmployees } from '../hooks/useEmployees' import { useWorkorder } from '../hooks/useWorkorder' import { useWorksheets } from '../hooks/useWorksheets' import { useCustomerAbo } from '../hooks/useCustomerAbo' import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite' import { elapsedLabel } from '../lib/elapsed' import { canAddOwnSupport } from '../lib/support' import { canEditWorksheetTimes, serviceTypeLabel } from '../lib/ticketForm' import { einrichtungAbrechnung, nachtragPlanFelder } from '../lib/planVorgabe' import { einrichtungText } from '../lib/stundenkontoView' import { worksheetSums } from '../lib/worksheetView' import { listBackPath, resolveTicketTab, ticketPath, ticketRef } from '../lib/ticketRoutes' import { statusSelectOptions, ticketTypeLabel } from '../lib/ticketStatus' import { appointmentText, assigneeOptions, dueText, ticketNumber } from '../lib/ticketView' import { createWorksheetFlow } from '../lib/worksheetFlow' // Ein Ticket als eigene Seite: /tickets/ (Details) und /tickets//arbeitsblaetter. // Alle Hooks stehen vor jeder Weiterleitung, sonst bricht der Wechsel /tickets/5/xyz -> /tickets/5 die Hook-Reihenfolge. export default function TicketPage() { const { ref, tab } = useParams() const location = useLocation() const { ticket, loading, error, notFound, updateWorkorder } = useWorkorder(ref) const activeTab = resolveTicketTab(tab) if (activeTab === null) return const back = if (loading) { return (
Wird geladen …
) } if (notFound) { return (
) } if (error || !ticket) { return (

Fehler: {error}

{back}
) } return } // Eine gelesene Uhrzeit oder Spanne ("09:30" bzw. "09:30–10:30") bricht nicht in sich um, nur davor am Komma. // Unlesbare Altwerte (Rohtext) brechen weiter frei um. const TIME_RANGE = /^\d{2}:\d{2}(–\d{2}:\d{2})?$/ function AppointmentText({ text }) { const at = text.lastIndexOf(', ') if (at === -1 || !TIME_RANGE.test(text.slice(at + 2))) return text return ( <> {text.slice(0, at)}, {text.slice(at + 2)} ) } function TicketView({ ticket, updateWorkorder, tab }) { const location = useLocation() const navigate = useNavigate() const { user, employee } = useAuth() // Einmal laden und an die Dialoge reichen (der Zuständig-Filter und die Unterstützer-Liste teilen sich die Antwort) const { employees, loading: employeesLoading, error: employeesError } = useEmployees() const employeesState = useMemo( () => ({ employees, loading: employeesLoading, error: employeesError }), [employees, employeesLoading, employeesError] ) const [showCreateWorksheet, setShowCreateWorksheet] = useState(false) const [editTimesWorksheet, setEditTimesWorksheet] = useState(null) const [ownSupportWorksheet, setOwnSupportWorksheet] = useState(null) // Hinweis des Arbeitsblatt-Flusses (z. B. "Zugesagt" ohne Kundenart); jedes neue Blatt setzt ihn neu const [hinweis, setHinweis] = useState('') // Werte, die gerade gespeichert werden: das Feld zeigt den neuen Wert und ist gesperrt, bei einem Fehler springt es zurück const [pending, setPending] = useState({}) const woid = ticket.woid || null const { worksheets, loading: worksheetsLoading, error: worksheetsError, createWorksheet, updateWorksheet, updateWorksheetTimes, saveOwnSupport, } = useWorksheets(woid) // Abo-Stundenkonto gibt es nur bei Wartungs-Abo; erst laden, wenn Arbeitsblätter gebraucht werden const { hoursAllowed: aboHoursAllowed } = useCustomerAbo( ticket.customerId, tab === 'arbeitsblaetter' || showCreateWorksheet ) // Einrichtungs-Ticket: 'abo' | 'projekt' | '' (V9); bei 'abo' zeigt die Seite das Stundenkonto des Kunden const einrichtung = einrichtungAbrechnung(ticket) const isPending = (field) => pending[field] !== undefined const shown = (field, current) => (isPending(field) ? pending[field] : current) // Fehler beim Schreiben bleiben still (wie bisher): bei Misserfolg ändert sich das Ticket nicht und das Feld springt zurück const saveField = async (field, value) => { if (isPending(field)) return setPending((prev) => ({ ...prev, [field]: value })) try { await updateWorkorder(ticket.$id, { [field]: value }) } finally { setPending((prev) => { const next = { ...prev } delete next[field] return next }) } } const goToTab = (id) => navigate(ticketPath(ticketRef(ticket), id), { state: location.state }) // Abo-Stunden: Arbeitszeit vom Stundenguthaben des Wartungs-Abos abziehen oder nicht const togglePlan = (ws) => updateWorksheet(ws.$id, { countsToPlan: !ws.countsToPlan }) const handleCreateWorksheet = async (worksheetData, currentUser) => { const result = await createWorksheetFlow({ ticket, worksheetData, user: currentUser, createWorksheet, updateTicket: updateWorkorder, updateCustomer: (id, data) => databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, id, data), getCustomer: (id) => databases.getDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, id), }) setHinweis(result?.hinweis || '') // Das neue Blatt soll sichtbar sein if (result?.success && tab !== 'arbeitsblaetter') goToTab('arbeitsblaetter') return result } const number = ticketNumber(ticket) const customerLabel = ticket.customerName || 'Unbekannt' const worksheetCount = !woid || worksheetsLoading ? undefined : worksheetsError ? '–' : worksheets.length const appointment = appointmentText(ticket) const due = dueText(ticket) return (
{ticket.customerId ? ( {customerLabel} ) : ( customerLabel )} {ticket.customerLocation ? ` · ${ticket.customerLocation}` : ''} } actions={ <> saveField('priority', priority)} disabled={isPending('priority')} /> } />
Termin
{appointment ? : Kein Termin}
Fällig
{due || '–'}
Einsatz
{serviceTypeLabel(ticket.serviceType) || '–'}
Angefragt von
{ticket.requestedBy || '–'}
Erstellt
{elapsedLabel(ticket)}
{hinweis &&

{hinweis}

} { if (id !== tab) goToTab(id) }} /> {einrichtung && ( <>

{einrichtungText(ticket)}

{einrichtung === 'abo' && ( )} )} {tab === 'details' && ( <>

{ticket.details || 'Keine Details vorhanden.'}

)} {tab === 'arbeitsblaetter' && ( !woid ? (

Für dieses Ticket gibt es keine Ticketnummer, Arbeitsblätter sind nicht möglich.

) : worksheetsError ? (

{worksheetsError}

) : ( <> canEditWorksheetTimes(ws, user, employee)} onEditTimes={setEditTimesWorksheet} canAddSupport={(ws) => canAddOwnSupport(ws, user, employee)} onOwnSupport={setOwnSupportWorksheet} currentUserId={user?.$id} /> ) )} setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} aboHoursAllowed={aboHoursAllowed} employeesState={employeesState} /> {ownSupportWorksheet && ( setOwnSupportWorksheet(null)} onSave={(ws, task) => saveOwnSupport(ws, task, user, employee)} /> )} {editTimesWorksheet && ( setEditTimesWorksheet(null)} onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee, nachtragPlanFelder({ worksheet: ws, ticket, aboHoursAllowed }))} employeesState={employeesState} /> )}
) }