import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, Navigate, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { PageHeader, Tabs, Button, EmptyState, StatusPill, UrgentPill } from '../components/ui'
import CreateTicketModal from '../components/CreateTicketModal'
import { useCustomers } from '../hooks/useCustomers'
import { useEmployees } from '../hooks/useEmployees'
import { useTicketCounts } from '../hooks/useTicketCounts'
import { useTicketMinutes } from '../hooks/useTicketMinutes'
import { useWorkorders } from '../hooks/useWorkorders'
import { elapsedLabel } from '../lib/elapsed'
import {
LIST_TABS,
listFilters,
nextLimit,
parseListParams,
serializeListParams,
withFilter,
woidRedirectTarget,
} from '../lib/ticketListUrl'
import { isRowActivation } from '../lib/rowClick'
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
import { TYPE_FILTERS, ticketTypeLabel } from '../lib/ticketStatus'
import { TIME_MAX_ROWS, minutesCellText } from '../lib/ticketTimes'
import { appointmentText, assigneeLabel, ticketNumber } from '../lib/ticketView'
// Pause nach dem letzten Tastendruck, bis der Suchtext in die Adresse übernommen wird
const SEARCH_DELAY_MS = 300
// Altlinks aus den Leads (Liste mit Suchteil woid=123) führen zur Ticketseite; dann läuft keine Abfrage der Liste
export default function TicketsPage() {
const [searchParams] = useSearchParams()
const target = woidRedirectTarget(searchParams)
if (target) return
return
}
function countText(n, searching) {
if (searching) return n === 1 ? '1 Treffer' : `${n} Treffer`
return n === 1 ? '1 Ticket' : `${n} Tickets`
}
function TicketList() {
const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation()
const navigate = useNavigate()
const state = useMemo(() => parseListParams(searchParams), [searchParams])
const filters = useMemo(() => listFilters(state), [state])
// Wohin "Zurück" auf der Ticketseite führt: dieselbe Liste mit denselben Filtern
const listSearch = location.search
// Jede Änderung geht in die Adresse und ersetzt den Eintrag (der Zurück-Knopf springt nicht durch Filterschritte)
const write = (change) => {
setSearchParams((prev) => serializeListParams(change(parseListParams(prev))), { replace: true })
}
const setFilter = (patch) => write((current) => withFilter(current, patch))
const loadMore = () => write((current) => ({ ...current, limit: nextLimit(current) }))
// Der Timer der Suche ruft die Schreib-Funktion des jüngsten Renders (setSearchParams liest die Adresse aus seinem Render)
const writeRef = useRef(write)
writeRef.current = write
// Suche: lokaler Text, Übernahme in die Adresse nach Tipp-Pause oder mit Enter.
// appliedRef merkt den zuletzt übernommenen (bzw. zuletzt aus der Adresse gelesenen) Suchtext:
// eigene Übernahmen laufen nicht in den Text zurück (sonst wären Leerzeichen am Ende weg),
// nur eine Änderung von außen (Zurück-Knopf, Link in der Seitenleiste) setzt den Text neu.
const [searchText, setSearchText] = useState(state.search)
const appliedRef = useRef(state.search)
const commitSearch = useCallback((text) => {
const next = text.trim()
if (next === appliedRef.current) return
appliedRef.current = next
writeRef.current((current) => withFilter(current, { search: next }))
}, [])
useEffect(() => {
if (state.search === appliedRef.current) return
appliedRef.current = state.search
setSearchText(state.search)
}, [state.search])
useEffect(() => {
if (searchText.trim() === appliedRef.current) return
const timer = setTimeout(() => commitSearch(searchText), SEARCH_DELAY_MS)
return () => clearTimeout(timer)
}, [searchText, commitSearch])
const { workorders, loading, error, createWorkorder } = useWorkorders(filters)
const { counts } = useTicketCounts({ typeId: state.typeId, urgentOnly: state.urgentOnly })
const { customers } = useCustomers()
// Einmal laden: die Liste zeigt Namen, der Dialog "Neues Ticket" braucht dieselben Mitarbeiter
const { employees } = useEmployees()
const { byWoid, capped, version } = useTicketMinutes(workorders)
// Mit welchem Limit wurden die gezeigten Zeilen geladen? Nach "Mehr laden" ist state.limit schon größer,
// die Zeilen sind es erst mit der Antwort: der Knopf bleibt bis dahin stehen (und ist gesperrt).
// Im Ruhezustand heißt das: Zeilen >= state.limit und state.limit unter der Obergrenze von nextLimit.
const [rowsFor, setRowsFor] = useState({ rows: workorders, limit: state.limit })
if (rowsFor.rows !== workorders) setRowsFor({ rows: workorders, limit: state.limit })
const hasMore =
!state.search && nextLimit({ limit: rowsFor.limit }) > rowsFor.limit && workorders.length >= rowsFor.limit
const [showCreate, setShowCreate] = useState(false)
const openCreate = () => setShowCreate(true)
const handleCreate = async (data) => {
const result = await createWorkorder(data)
if (result.success) {
setShowCreate(false)
navigate(ticketPath(ticketRef(result.data)), { state: { listSearch } })
}
return result
}
// Zähler: bei Suche aus, bei Fehler "–", vor der ersten Antwort fehlt die Zahl
const tabs = LIST_TABS.map((tab) => {
const count = state.search || !(tab.id in counts) ? undefined : counts[tab.id] === null ? '–' : counts[tab.id]
return { id: tab.id, label: tab.label, count }
})
// Die Art steht in der Zeile, solange der Filter keine einzelne Art festlegt
const showType = state.typeId === 'orders' || state.typeId === 'all'
// byWoid ist eine an Ort und Stelle ergänzte Map: erst `version` zeigt eine Änderung an
const rows = useMemo(
() =>
workorders.map((ticket, index) => {
const path = ticketPath(ticketRef(ticket))
const assignee = assigneeLabel(ticket, employees)
return (
{
if (isRowActivation(e)) navigate(path, { state: { listSearch } })
}}
>
{ticketNumber(ticket)}
{ticket.topic || ticket.title || 'Kein Betreff'}
{ticket.customerName || 'Unbekannt'}
{ticket.customerLocation ? ` · ${ticket.customerLocation}` : ''}
{showType && ticket.type ? ` · ${ticketTypeLabel(ticket.type)}` : ''}
{assignee.none ? {assignee.text} : assignee.text}
{appointmentText(ticket) || '–'}
{minutesCellText(ticket, index, byWoid)}
{elapsedLabel(ticket)}
)
}),
[workorders, employees, byWoid, version, listSearch, navigate, showType]
)
let content
// Ladezustand, solange keine Zeilen da sind (erstes Laden und Filterwechsel auf eine leere Liste); mit Zeilen bleiben die alten stehen
if (loading && workorders.length === 0) {
content = Wird geladen …
} else if (error) {
content = Fehler: {error}
} else if (workorders.length === 0) {
content = (
Neues Ticket}
/>
)
} else {
content = (
<>
Ticket
Status
Zuständig
Termin
Zeit
Erstellt
{rows}
{countText(workorders.length, Boolean(state.search))}
{loading && · Wird geladen … }
{capped && · Zeit nur für die ersten {TIME_MAX_ROWS} Zeilen berechnet }
Neueste zuerst
{hasMore && (
Mehr laden
)}
>
)
}
return (
Neues Ticket}
/>
{ if (id !== state.tab) setFilter({ tab: id }) }}
/>
{content}
setShowCreate(false)}
onCreate={handleCreate}
customers={customers}
employees={employees}
/>
)
}