import { useEffect, useState, useCallback, useMemo } from 'react'; import { useParams, useLocation, useNavigate } from 'react-router-dom'; import { pb } from '../lib/pocketbase'; import { computeROTotals, roServiceTotal } from '../lib/totals'; import type { RepairOrder, ROService, ServiceItem, ROEvent, ROFinancialAuditEntry } from '../types'; import { getStatusConfig } from '../lib/status'; import { formatCurrency, formatDateTime, formatElapsed } from '../lib/format'; import { useToast } from '../components/ui/Toast'; import LoadingSpinner from '../components/ui/LoadingSpinner'; import { X, Wrench, Search, Plus, Check, History, FileText, Printer, Download, Mail, Phone, Ban, Clock3, ArrowLeft, AlertTriangle, DollarSign, Tag, Package, Trash2, Loader2, Layers, Save, BellRing } from 'lucide-react'; import { readEvents, appendROEvent, readPromisedOverride, setPromisedOverride, isVoided, setVoided, clockRunning, clockElapsedMinutes, clockStart, clockStop } from '../lib/roEvents'; import { openEmailDraft, openSmsDraft } from '../lib/contactLinks'; import { downloadROPDF, printROPDF } from '../lib/roPdf'; import { useSettings } from '../store/settings'; import { logError } from '../lib/userMessages'; import { parseRepairOrderServices, syncAssignmentsForROServices } from '../lib/technicianData'; import { formatServiceTextFields } from '../lib/dataFormatting'; import { spellCheckProps } from '../lib/spellcheck'; type TabId = 'details' | 'services' | 'financial' | 'history' | 'timeline'; const TABS: { id: TabId; label: string; icon: typeof Wrench }[] = [ { id: 'details', label: 'Details', icon: FileText }, { id: 'services', label: 'Services', icon: Wrench }, { id: 'financial', label: 'Financial', icon: DollarSign }, { id: 'history', label: 'History', icon: History }, { id: 'timeline', label: 'Timeline', icon: Clock3 }, ]; /* ── Shared helpers (inlined from ServiceCatalog) ─── */ function getServiceLaborPrice(service: Pick): number { return (Number(service.laborHours) || 0) * (Number(service.laborRate) || 0); } function getServicePricingMode(service: Pick): 'full' | 'split' { return getServiceLaborPrice(service) > 0 || (Number(service.partsCost) || 0) > 0 ? 'split' : 'full'; } interface ServiceFormState { name: string; category: string; description: string; pricingMode: 'full' | 'split'; fullPrice: number; laborPrice: number; partsPrice: number; } function createEmptyServiceForm(): ServiceFormState { return { name: '', category: '', description: '', pricingMode: 'full', fullPrice: 0, laborPrice: 0, partsPrice: 0, }; } /* ── Helpers ───────────────────────────────────────── */ function formatDueDateTime(ro: RepairOrder): string { const override = readPromisedOverride(ro); if (override) return formatDateTime(override); if (ro.promisedTime) return formatDateTime(ro.promisedTime); if (ro.writeupTime) { const d = new Date(ro.writeupTime); d.setHours(d.getHours() + 4); return formatDateTime(d.toISOString()); } return '—'; } function formatTimeRemaining(ro: RepairOrder, now: number): string { const override = readPromisedOverride(ro); const target = override || ro.promisedTime || (ro.writeupTime ? new Date(new Date(ro.writeupTime).getTime() + 4 * 60 * 60 * 1000).toISOString() : ''); if (!target) return '—'; const diff = new Date(target).getTime() - now; if (diff <= 0) return 'Overdue'; const hours = Math.floor(diff / 3600000); const minutes = Math.floor((diff % 3600000) / 60000); return `${hours}h ${minutes}m`; } function formatCompletedTime(ro: RepairOrder): string { return formatDateTime(ro.completedTime || ro.completedDate || ''); } function getUrgencyClass(ro: RepairOrder, now: number): '' | 'warning' | 'urgent' { const override = readPromisedOverride(ro); const target = override || ro.promisedTime || (ro.writeupTime ? new Date(new Date(ro.writeupTime).getTime() + 4 * 60 * 60 * 1000).toISOString() : ''); if (!target) return ''; const diff = new Date(target).getTime() - now; if (diff < 0) return 'urgent'; if (diff < 60 * 60 * 1000) return 'warning'; return ''; } function StatusBadge({ status, config }: { status: string; config: Record }) { const cfg = config[status as keyof typeof config] || { label: status, colors: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' }; return {cfg.label}; } function TimelineEntry({ ev }: { ev: ROEvent }) { const desc = (() => { switch (ev.type) { case 'created': return 'Repair order created'; case 'edit': return 'Repair order edited'; case 'status': return `Status changed: ${ev.data?.from} → ${ev.data?.to}`; case 'add_time': return `Added ${ev.data?.minutes} min (total: ${ev.data?.newTotal} min)`; case 'clock_start': return 'Technician clock started'; case 'clock_stop': return `Technician clock stopped (${ev.data?.elapsed} min)`; case 'satisfaction': return `Customer rated ${ev.data?.rating}★`; case 'notify': return `Notified via ${ev.data?.channel} to ${ev.data?.to}`; case 'financial': return `Financial field updated: ${ev.data?.field}`; default: return ev.type; } })(); return (
  • {desc}

    {formatDateTime(ev.at)}

  • ); } /* ── AddTimeModal ──────────────────────────────────── */ function AddTimeModal({ open, initialMinutes, onCancel, onConfirm }: { open: boolean; initialMinutes: number; onCancel: () => void; onConfirm: (minutes: number) => void }) { const [minutes, setMinutes] = useState(initialMinutes); useEffect(() => { setMinutes(initialMinutes); }, [initialMinutes]); if (!open) return null; return (
    e.stopPropagation()}>

    Add Time

    Extend the remaining time on this RO.

    setMinutes(Number(e.target.value) || 1)} className="mt-3 w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" />
    ); } /* ── Service Row (editable RO service row) ─────────── */ function PricingBadge({ mode }: { mode: 'full' | 'split' }) { if (mode === 'full') { return Full Price; } return Split; } function EditableServiceRow({ service, index, onChange, onRemove, defaultLaborRate = 145, spellCheck, lang }: { service: Partial; index: number; onChange: (index: number, field: string, value: string | number | boolean) => void; onRemove: (index: number) => void; defaultLaborRate?: number; spellCheck?: boolean; lang?: string; }) { const pricingMode = service.pricingMode || 'full'; const isSplit = pricingMode === 'split'; const total = roServiceTotal(service); const scOn = service.applyShopCharge !== false; return (
    Service #{index + 1} {/* Pricing mode toggle */}
    onChange(index, 'name', e.target.value)} placeholder="Service name" className="mt-0.5 w-full rounded border border-gray-300 bg-white px-2 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" />