import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Badge, Button, ConfirmDialog, DataTable, Dialog, EmptyState, ErrorState, FilterChips, FormField, FormGrid, Notice, PageHeader, Skeleton, Toolbar, useToast, } from '@sims/ui' import { createTicket, getBranches, getClients, getEmployees, getTickets, patchTicket, staffId, TICKET_STATUSES, type Branch, type Client, type Employee, type Ticket, type TicketStatus, } from '../api' import { useData } from './Clients' const PAGE_SIZE = 50 /** The five APEX service lines (D20 design §1) — suggestions only; module stays free text. */ export const TICKET_MODULE_CODES = ['SMS', 'RTGS', 'WHATSAPP', 'MOBILEAPP', 'ATM'] export const TICKET_STATUS_LABEL: Record = { open: 'Open', in_progress: 'In progress', waiting: 'Waiting', closed: 'Closed', dropped: 'Dropped', } export const TICKET_TONE: Record = { open: 'accent', in_progress: 'warn', waiting: undefined, closed: 'ok', dropped: 'err', } /** Description cell: truncated with the full text on hover (title). */ export function TicketDescription(props: { text: string }) { if (props.text === '') return <>— return ( {props.text} ) } /** * The workbench transitions: open→Start/Waiting, in progress→Waiting/Close, * waiting→Start/Close, terminal→Reopen; Drop asks first (it stays on record). */ export function TicketActions(props: { t: Ticket; onDone: () => void; onError: (m: string) => void }) { const [busy, setBusy] = useState(false) const [dropping, setDropping] = useState(false) const go = (status: TicketStatus) => { if (busy) return setBusy(true) patchTicket(props.t.id, { status }) .then(props.onDone) .catch((e: Error) => props.onError(e.message)) .finally(() => setBusy(false)) } const s = props.t.status return ( {(s === 'open' || s === 'waiting') && } {(s === 'open' || s === 'in_progress') && } {(s === 'in_progress' || s === 'waiting') && } {(s === 'closed' || s === 'dropped') && } {s !== 'closed' && s !== 'dropped' && ( )} {dropping && ( setDropping(false)} onConfirm={() => { setDropping(false); go('dropped') }} title="Drop ticket" body={`Drop this ${props.t.kind !== '' ? `${props.t.kind} ` : ''}ticket for ${props.t.clientName}? It stays on record as dropped and can be reopened.`} confirmLabel="Drop" tone="danger" /> )} ) } /** Per-row assignment — '' = unassigned (PATCH sends null to clear). */ export function TicketAssignSelect(props: { t: Ticket; employees: Employee[]; onDone: () => void; onError: (m: string) => void }) { return ( ) } /** * New-ticket dialog. Client via type-ahead (Modules assign pattern) unless the * caller pre-picks one (Client 360 tab); branches load once a client is picked; * module and kind are free text with suggestions (config over code — kinds come * from history via the list response). */ export function NewTicketDialog(props: { open: boolean; onClose: () => void; onCreated: () => void kinds: string[] /** Pre-picked client (Client 360) — hides the type-ahead. */ initialClient?: { id: string; code: string; name: string } }) { const toast = useToast() const [q, setQ] = useState('') const [hits, setHits] = useState([]) const [pick, setPick] = useState<{ id: string; code: string; name: string } | undefined>() const [branches, setBranches] = useState([]) const [f, setF] = useState({ branchId: '', moduleCode: '', kind: '', description: '', onlineOffline: '' }) const [error, setError] = useState() const [saving, setSaving] = useState(false) useEffect(() => { if (!props.open) return setError(undefined) setQ(''); setHits([]); setPick(props.initialClient) setF({ branchId: '', moduleCode: '', kind: '', description: '', onlineOffline: '' }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.open, props.initialClient?.id]) useEffect(() => { if (pick === undefined) { setBranches([]); return } getBranches(pick.id).then(setBranches).catch(() => setBranches([])) }, [pick]) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const search = (text: string) => { setQ(text); setPick(undefined); setF((p) => ({ ...p, branchId: '' })) if (text.trim().length < 2) { setHits([]); return } getClients(text).then((cs) => setHits(cs.slice(0, 6))).catch(() => setHits([])) } const save = () => { if (saving) return if (pick === undefined) { setError('Pick a client'); return } setError(undefined) setSaving(true) createTicket({ clientId: pick.id, ...(f.branchId !== '' ? { branchId: f.branchId } : {}), ...(f.moduleCode.trim() !== '' ? { moduleCode: f.moduleCode.trim() } : {}), ...(f.kind.trim() !== '' ? { kind: f.kind.trim() } : {}), ...(f.description.trim() !== '' ? { description: f.description.trim() } : {}), ...(f.onlineOffline !== '' ? { onlineOffline: f.onlineOffline } : {}), }) .then(() => { toast.ok('Ticket created'); props.onCreated(); props.onClose() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return ( { if (!saving) props.onClose() }} title="New ticket" footer={ <> } > {props.initialClient !== undefined ? : ( <> search(e.target.value)} /> {pick === undefined && hits.length > 0 && (
{hits.map((c) => ( ))}
)} )}