// §3.8 Rate Health Dashboard — Completeness, Expiry, Validation function RecalcOverlay() { return (
Recalculating…
); } function RateHealth({ user, perms, onAddRate, onOpenRate, refreshKey, onValidationData }) { const [tab, setTab] = useState("completeness"); const [completenessData, setCompletenessData] = useState(null); const [expiryData, setExpiryData] = useState(null); const [validationData, setValidationData] = useState(null); const [loading, setLoading] = useState(true); const [localRefresh, setLocalRefresh] = useState(0); const [selectedTerminal, setSelectedTerminal] = useState(null); const triggerRefresh = () => setLocalRefresh(n => n + 1); useEffect(() => { if (!user || user.role === "unknown") return; setLoading(true); Promise.all([ apiFetch("/api/health/completeness"), apiFetch("/api/health/expiry?window=90"), apiFetch("/api/health/validation"), ]).then(([c, e, v]) => { setCompletenessData(c || { rows: [], summary: {} }); setExpiryData(e || { expiring: [], expired: [] }); const vd = v || { findings: [], rules: [], last_run: null, rates_scanned: 0 }; setValidationData(vd); onValidationData?.(vd); }).catch(() => { setCompletenessData({ rows: [], summary: {} }); setExpiryData({ expiring: [], expired: [] }); setValidationData({ findings: [], rules: [], last_run: null, rates_scanned: 0 }); }).finally(() => setLoading(false)); }, [user?.role, refreshKey, localRefresh]); const tabs = [ { id: "completeness", label: "Completeness" }, { id: "expiry", label: "Expiry Alerts", count: expiryData?.expiring?.length }, { id: "validation", label: "Quality Validation" }, ]; return (
{(perms.edit || perms.admin) && ( )} } /> setSelectedTerminal(prev => prev === t ? null : t)} /> {selectedTerminal && (
Filtered by terminal: {selectedTerminal}
)} {tab === "completeness" && } {tab === "expiry" && } {tab === "validation" && }
); } function HealthKPIs({ completeness, expiry, validation, loading }) { const rows = completeness?.rows || []; const totalExpected = rows.length; const present = rows.filter(c => c.status === "Present").length; const missing = rows.filter(c => c.status === "Missing").length; const overallPct = totalExpected > 0 ? Math.round((present / totalExpected) * 100) : 0; const expiringCount = expiry?.expiring?.length ?? 0; const findings = validation?.findings || []; const errCount = findings.filter(r => r.severity === "Error").length; const warnCount = findings.filter(r => r.severity === "Warning").length; const dash = loading ? "—" : null; return (
Overall Completeness
{dash ?? `${overallPct}%`}
{!loading && }
{dash ?? `${present} / ${totalExpected} combinations`}
Missing Rates
5 ? "var(--red)" : "var(--ink)" }}>{dash ?? missing}
across terminals · click to add
Expiring < 90 days
{dash ?? expiringCount}
active rates approaching expiry
Open Validation Issues
{dash ?? findings.length}
{loading ? "—" : ( <> {errCount} errors · {warnCount} warnings )}
); } // ============== COMPLETENESS PANEL ============== function Completeness({ data, loading, onAddRate, terminalFilter }) { const DEFAULT_FILTERS = {}; const [colFilters, setColFilters] = useState(DEFAULT_FILTERS); const [openDropdown, setOpenDropdown] = useState(null); const [dropdownSearch, setDropdownSearch] = useState(""); const handleAddRate = async (r) => { if (!onAddRate) return; const dimFields = { terminal_id: r.terminal_id, cost_type_id: r.cost_type_id, containertype: r.equipment_type_id || "", container_size: r.container_size === "All" ? "" : r.container_size, container_fullness: r.container_fullness === "All" ? "" : r.container_fullness, shipment_type: r.shipment_type_id || "", brand_company_id: r.brand_company_id || "", }; try { const rates = await apiFetch(`/api/rates?terminal_id=${r.terminal_id}&cost_type_id=${r.cost_type_id}&limit=10`); if (rates && rates.length > 0) { const today = Date.now(); const ref = rates.slice().sort((a, b) => { const da = a.effective_date ? Math.abs(new Date(a.effective_date) - today) : Infinity; const db = b.effective_date ? Math.abs(new Date(b.effective_date) - today) : Infinity; return da - db; })[0]; onAddRate({ ...dimFields, brand_company_id: dimFields.brand_company_id || ref.brand_company_id || "", cost_category: ref.cost_category || "", effective_date: ref.effective_date || "", expiry_date: ref.expiry_date || "", charge_unit_id: ref.charge_unit_id || "", primary_currency: ref.primary_currency || "", contract_owner: ref.contract_owner || "", transport_type: ref.transport_type || "", payment_terms: ref.payment_terms || "", payable_by: ref.payable_by || "", sap_vendor_code: ref.sap_vendor_code || "", slab_unit: ref.slab_unit || "", slab_type: ref.slab_type || "", is_invoicerate: ref.is_invoicerate ?? "", stcy_flag: ref.stcy_flag ?? "", calculator: ref.calculator ?? "", contract_ref: ref.contract_ref || "", route: ref.route || "", corridor_from: ref.corridor_from || "", corridor_to: ref.corridor_to || "", }); } else { onAddRate(dimFields); } } catch { onAddRate(dimFields); } }; const allRows = (data?.rows || []).filter(r => r.status === "Missing" && (!terminalFilter || r.terminal === terminalFilter)); const summary = data?.summary || {}; // Apply column filters on Missing rows only const filtered = allRows.filter(r => Object.entries(colFilters).every(([key, sel]) => !sel.length || sel.includes(String(r[key] ?? "")) ) ); const activeFilterCount = Object.values(colFilters).filter(v => v.length > 0).length; // Close dropdown on outside click useEffect(() => { const h = e => { if (!e.target.closest("[data-fdrop]")) { setOpenDropdown(null); setDropdownSearch(""); } }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); // Column header with filter dropdown — options always from allRows (stable) const fDrop = (key, header, labelFn) => { const options = [...new Set(allRows.map(r => String(r[key] ?? "")).filter(Boolean))].sort(); const sel = colFilters[key] || []; const active = sel.length > 0; const label = v => labelFn ? labelFn(v) : v; return (
{header}
e.stopPropagation()}> {openDropdown === key && (
setDropdownSearch(e.target.value)} placeholder="Search…" style={{ width: "100%", padding: "3px 7px", fontSize: 12, border: "1px solid var(--border)", borderRadius: 3, outline: "none", boxSizing: "border-box", background: "var(--paper)" }} />
{(() => { const vis = options.filter(v => !dropdownSearch || label(v).toLowerCase().includes(dropdownSearch.toLowerCase())); return ( <>
{vis.map(v => ( ))} {vis.length === 0 &&
No options
}
); })()}
)}
); }; // Terminal label helper for dropdown const terminalLabel = v => { const s = summary[v]; return s ? `${s.terminal_name} (${v})` : v; }; return (
1 ? "s" : ""} active)` : ""}`} sub="click a Missing row to add rate" right={
{activeFilterCount > 0 && ( )}
Missing Expiring
}>
{loading && data && } {fDrop("terminal", "Terminal", terminalLabel)} {fDrop("cost_type_name", "Cost Type")} {fDrop("equipment", "Equipment")} {fDrop("container_size", "Size")} {fDrop("container_fullness","Fullness")} {fDrop("shipment_type", "Shipment Type")} {fDrop("brand_company", "Brand")} {filtered.map((r, i) => ( handleAddRate(r)} style={{ cursor: "pointer", background: "#fef4f4" }}> ))} {!loading && filtered.length === 0 && ( )}
Last Updated
{r.terminal} {r.cost_type_name} {r.equipment} {r.container_size} {r.container_fullness} {r.shipment_type} {r.brand_company} {r.last_updated || "—"}
No rows match the current filters
); } // ============== TERMINAL COVERAGE CARDS (always visible above tabs) ============== function TerminalCoverageCards({ data, validation, loading, selected, onSelect }) { const summary = data?.summary || {}; // Build per-terminal QV counts; also capture terminal_name from findings const qvByTerminal = {}; const qvTerminalName = {}; (validation?.findings || []).forEach(f => { if (!f.terminal) return; qvByTerminal[f.terminal] = (qvByTerminal[f.terminal] || 0) + 1; if (f.terminal_name && !qvTerminalName[f.terminal]) qvTerminalName[f.terminal] = f.terminal_name; }); // Terminals from coverage matrix const summaries = Object.entries(summary).map(([code, s]) => ({ code, name: s.terminal_name || code, present: s.present, missing: s.missing, expiring: s.expiring, total: s.total, qv: qvByTerminal[code] || 0, pct: s.total > 0 ? Math.round((s.present / s.total) * 100) : 0, qvOnly: false, })).sort((a, b) => a.pct - b.pct); // Terminals that only appear in QV findings (not in coverage matrix) const coveredCodes = new Set(Object.keys(summary)); const qvOnlySummaries = Object.entries(qvByTerminal) .filter(([code]) => !coveredCodes.has(code)) .map(([code, qv]) => ({ code, name: qvTerminalName[code] || code, present: null, missing: null, expiring: null, total: null, qv, pct: null, qvOnly: true, })).sort((a, b) => b.qv - a.qv); const allSummaries = [...summaries, ...qvOnlySummaries]; if (!loading && allSummaries.length === 0) return null; return (
{loading && allSummaries.length === 0 ? [1,2,3].map(i =>
) : allSummaries.map(s => { const isSelected = selected === s.code; const ringColor = s.pct >= 80 ? "var(--green)" : s.pct >= 50 ? "var(--amber)" : "var(--red)"; return (
onSelect?.(s.code)} className="kpi" style={{ cursor: "pointer", transition: "border-color .15s, box-shadow .15s, background .15s", ...(isSelected ? { border: "2px solid var(--teal)", background: "rgba(0,158,135,.06)", boxShadow: "0 0 0 3px rgba(0,158,135,.15)" } : { border: "1px solid var(--line)" }), }}>
{s.qvOnly ?
QV
: }
{s.name}
{s.code}
{isSelected && }
{!s.qvOnly && ● {s.present} present} {!s.qvOnly && ● {s.missing} missing} {!s.qvOnly && ● {s.expiring} expiring} {s.qv > 0 && ● {s.qv} QV issues}
); }) }
); } // ============== RENEW MODAL ============== function RenewModal({ rate, fullRate, loadingRate, loadError, onClose, onModify, onCreateNew, onSetInactive, creating }) { const [inactiveClicked, setInactiveClicked] = useState(false); const expiryDate = fullRate?.expiry_date || rate.expiry; const newEffective = expiryDate ? new Date(new Date(expiryDate).getTime() + 86400000).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10); const isFuture = newEffective > today; const busy = loadingRate || creating; const hasFullRate = !!fullRate?.id; const hasSuccessor = !!rate.has_successor; const optionStyle = (disabled) => ({ textAlign: "left", padding: "14px 16px", border: `2px solid ${disabled ? "var(--line)" : "var(--border)"}`, borderRadius: 8, background: disabled ? "var(--surface)" : "#fff", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .55 : 1, transition: "border-color .15s", }); const hoverIn = (e, disabled) => { if (!disabled) e.currentTarget.style.borderColor = "var(--teal)"; }; const hoverOut = (e, disabled) => { e.currentTarget.style.borderColor = disabled ? "var(--line)" : "var(--border)"; }; return (
e.stopPropagation()}> {/* Header */}
Renew Rate · {rate.id}
{rate.terminal} · {rate.cost_type} · expired {rate.expiry}
{/* Body */}
{loadingRate && (
Loading rate details…
)} {loadError && (
{loadError}
)} {/* Successor warning banner */} {hasSuccessor && (
⚠️
A successor rate already exists
This rate has a follow-up rate already created. The recommended action is to set this rate to Inactive to clean up the status.
)} {/* Primary action when successor exists: Set Inactive */} {hasSuccessor && ( )} {/* Divider when successor exists */} {hasSuccessor && (
or choose another action
)} {/* Option 1: Extend */} {/* Option 2: Create successor */}
{/* Footer */}
); } // ============== BULK RENEW MODAL ============== function BulkRenewModal({ rates, onClose, onDone }) { const [mode, setMode] = useState(null); // null | "extend" | "create" const [effectiveDate, setEffectiveDate] = useState(""); const [expiryDate, setExpiryDate] = useState(""); const [processing, setProcessing] = useState(false); const [progress, setProgress] = useState(null); // null | { done, total, errors[] } const today = new Date().toISOString().slice(0, 10); const canConfirm = mode && expiryDate && (mode === "extend" || effectiveDate) && !processing; const isDone = progress && progress.done >= rates.length; const handleConfirm = async () => { setProcessing(true); const errors = []; setProgress({ done: 0, total: rates.length, errors: [] }); if (mode === "extend") { // Batch PATCH — one request for all const ids = rates.map(r => parseInt(String(r.id).replace(/^R-/i, ""), 10)).filter(Boolean); try { await apiFetch("/api/rates/batch", { method: "PATCH", body: JSON.stringify({ ids, updates: { expiry_date: expiryDate } }), }); setProgress({ done: rates.length, total: rates.length, errors: [] }); } catch (e) { errors.push(...rates.map(r => r.id)); setProgress({ done: rates.length, total: rates.length, errors }); } } else { // Create successor: single batch request — backend handles set-Inactive + INSERT in one transaction const ids = rates.map(r => parseInt(String(r.id).replace(/^R-/i, ""), 10)).filter(Boolean); try { const res = await apiFetch("/api/rates/batch-renew", { method: "POST", body: JSON.stringify({ ids, effective_date: effectiveDate, expiry_date: expiryDate || null }), }); const failedIds = (res?.errors || []).map(e => e.split(":")[0]); setProgress({ done: rates.length, total: rates.length, errors: failedIds }); } catch (e) { setProgress({ done: rates.length, total: rates.length, errors: rates.map(r => r.id) }); } } setProcessing(false); if (errors.length === 0) { onDone(); } }; const optCard = (id, title, desc) => ( ); return (
e.stopPropagation()}> {/* Header */}
Bulk Renew
{rates.length} rate{rates.length !== 1 ? "s" : ""} selected
{/* Body */}
{optCard("extend", "Extend expiry date", "Update the expiry date for all selected rates in place. No new records are created.")} {optCard("create", "Create successor rates", "If effective date ≤ today: current rates → Inactive, new rates → Active. If future: current rates unchanged, new rates → Schedule.")} {/* Date fields — expand when mode chosen */} {mode && (
{mode === "create" && ( )}
{mode === "create" && effectiveDate && (() => { const isFuture = effectiveDate > today; return (
{isFuture ? "⚠ Future date — current rates remain Active; new rates will be set to Schedule." : "✓ Past/today date — current rates will be set to Inactive; new rates will be Active."}
); })()}
)} {/* Progress */} {progress && (
{isDone && progress.errors.length === 0 ? ✓ All {rates.length} rate{rates.length !== 1 ? "s" : ""} updated successfully :
Processing {progress.done} / {progress.total}…
{progress.errors.length > 0 && (
Failed: {progress.errors.join(", ")}
)}
}
)}
{/* Footer */}
{isDone && progress.errors.length === 0 ? : <> }
); } // ============== EXPIRY PANEL ============== function ExpiryAlerts({ data, loading, onAddRate, onRefresh, terminalFilter }) { const [window, setWindow] = useState(60); const [renewModal, setRenewModal] = useState(null); // { rate, fullRate, loadingRate, loadError } const [creating, setCreating] = useState(false); const [filters, setFilters] = useState({}); const [openDropdown, setOpenDropdown] = useState(null); const [dropdownSearch, setDropdownSearch] = useState(""); const [selected, setSelected] = useState(new Set()); // Set of rate id strings const [bulkModal, setBulkModal] = useState(false); const isExpired = window === "expired"; const allRows = (isExpired ? (data?.expired || []) : (data?.expiring || []).filter(a => a.days <= window) ).filter(r => !terminalFilter || r.terminal === terminalFilter); const handleRenew = (r) => { // Show modal immediately, then load full rate in background setRenewModal({ rate: r, fullRate: null, loadingRate: true, loadError: null }); const numericId = String(r.id).replace(/^R-/i, ""); apiFetch(`/api/rates/${numericId}`) .then(fullRate => { if (fullRate?.id) { setRenewModal(prev => prev ? { ...prev, fullRate, loadingRate: false } : null); } else { setRenewModal(prev => prev ? { ...prev, loadingRate: false, loadError: "Rate not found" } : null); } }) .catch(e => { setRenewModal(prev => prev ? { ...prev, loadingRate: false, loadError: e.message || "Failed to load rate details" } : null); }); }; const handleModify = () => { if (!renewModal) return; onAddRate(renewModal.fullRate); setRenewModal(null); }; const handleSetInactive = async () => { if (!renewModal?.fullRate) return; const { fullRate } = renewModal; setCreating(true); try { const { status_id, terminal_code, terminal_name, cost_type_code, cost_type_name, charge_unit_code, containertype_code, shipment_type_code, iso_code, slab_unit_code, slab_type_code, vendor_name, material_name, brand_company_code, brand_company_name, created_by, created_at, modified_by, modified_at, ...body } = fullRate; await apiFetch(`/api/rates/${fullRate.id}`, { method: "PUT", body: JSON.stringify({ ...body, status: "Inactive" }), }); } catch { /* ignore */ } setCreating(false); setRenewModal(null); onRefresh?.(); }; const handleCreateNew = async () => { if (!renewModal) return; const { fullRate } = renewModal; const expiryDate = fullRate.expiry_date; const newEffective = expiryDate ? new Date(new Date(expiryDate).getTime() + 86400000).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10); const isFuture = newEffective > today; if (!isFuture) { setCreating(true); try { // Set old rate → Inactive before opening new rate form const { status_id, terminal_code, terminal_name, cost_type_code, cost_type_name, charge_unit_code, containertype_code, shipment_type_code, iso_code, slab_unit_code, slab_type_code, vendor_name, material_name, brand_company_code, brand_company_name, created_by, created_at, modified_by, modified_at, ...body } = fullRate; await apiFetch(`/api/rates/${fullRate.id}`, { method: "PUT", body: JSON.stringify({ ...body, status: "Inactive" }), }); } catch { /* continue even if update fails */ } setCreating(false); } const { id, expiry_date, created_at, modified_at, status_id, terminal_code, terminal_name, cost_type_code, cost_type_name, charge_unit_code, containertype_code, shipment_type_code, iso_code, slab_unit_code, slab_type_code, vendor_name, material_name, brand_company_code, brand_company_name, created_by, modified_by, ...rest } = fullRate; onAddRate({ ...rest, effective_date: newEffective, expiry_date: "", status: isFuture ? "Schedule" : "Active" }); setRenewModal(null); }; // Reset column filters when switching window useEffect(() => { setFilters({}); setSelected(new Set()); }, [window]); const applyFilters = (rows, f) => rows.filter(r => Object.entries(f).every(([k, sel]) => !sel.length || sel.includes(String(r[k] ?? "")))); const displayRows = applyFilters(allRows, filters); useEffect(() => { const h = e => { if (!e.target.closest("[data-fdrop]")) { setOpenDropdown(null); setDropdownSearch(""); } }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); const fDrop = (key, header, labelFn) => { const options = [...new Set(allRows.map(r => String(r[key] ?? "")).filter(Boolean))].sort(); const sel = filters[key] || []; const active = sel.length > 0; const dropId = `r-${key}`; const label = v => labelFn ? labelFn(v) : v; return (
{header}
e.stopPropagation()}> {openDropdown === dropId && (
setDropdownSearch(e.target.value)} placeholder="Search…" style={{ width: "100%", padding: "3px 6px", fontSize: 11, border: "1px solid var(--border)", borderRadius: 3, background: "var(--paper)", color: "var(--ink)" }} />
{options.filter(v => !dropdownSearch || v.toLowerCase().includes(dropdownSearch.toLowerCase())).map(v => ( ))}
)}
); }; const activeFilterCount = Object.values(filters).filter(v => v.length).length; return (
{renewModal && ( setRenewModal(null)} onModify={handleModify} onCreateNew={handleCreateNew} onSetInactive={handleSetInactive} creating={creating} /> )}
Alert window
{[ { v: 30, label: "30 days" }, { v: 60, label: "60 days" }, { v: 90, label: "90 days" }, { v: "expired", label: "Expired" }, ].map(({ v, label }) => ( ))}
{!isExpired && (
≤ 14 days 15–30 days 31–60 days
)}
{/* Bulk action bar — only for non-expired mode */} {!isExpired && selected.size > 0 && (
{selected.size} rate{selected.size !== 1 ? "s" : ""} selected
)} {bulkModal && !isExpired && ( selected.has(String(r.id)))} onClose={() => setBulkModal(false)} onDone={() => { setBulkModal(false); setSelected(new Set()); onRefresh?.(); }} /> )} 0 && ( )}>
{loading && data && } {!isExpired && ( )} {fDrop("terminal", "Terminal")} {fDrop("cost_type", "Cost Type")} {fDrop("equipment", "Equipment")} {fDrop("container_size", "Size")} {fDrop("container_fullness", "Fullness")} {fDrop("shipment_type", "Shipment")} {fDrop("brand", "Brand")} {!isExpired && } {fDrop("expiry", isExpired ? "Expired on" : "Expiry")} {!isExpired && fDrop("focal", "Focal")} {displayRows.sort((a, b) => a.days - b.days).map(r => { const expired = r.days < 0; const isSelected = selected.has(String(r.id)); const color = expired ? "var(--red)" : r.days <= 14 ? "var(--red)" : r.days <= 30 ? "var(--amber)" : "#d8a83a"; return ( {!isExpired && ( )} {!isExpired && ( )} {!isExpired && } ); })} {!loading && displayRows.length === 0 && ( )}
0 && displayRows.every(r => selected.has(String(r.id)))} onChange={e => { if (e.target.checked) { setSelected(new Set(displayRows.map(r => String(r.id)))); } else { setSelected(new Set()); } }} style={{ cursor: "pointer" }} /> Rate IDRate{isExpired ? "Overdue" : "Days Left"}
{ const id = String(r.id); setSelected(prev => { const next = new Set(prev); e.target.checked ? next.add(id) : next.delete(id); return next; }); }} style={{ cursor: "pointer" }} /> {r.id} {r.terminal} {r.cost_type} {r.equipment || "—"} {r.container_size || "—"} {r.container_fullness || "—"} {r.shipment_type || "—"} {r.brand || "—"}{r.rate != null ? fmt.num(r.rate) : "—"} {r.ccy}{r.expiry}
{expired ? `${Math.abs(r.days)}d ago` : `${r.days}d`}
{r.focal} {!isExpired && }
{isExpired ? "No expired rates found" : `No rates expiring within ${window} days`}
); } // ============== VALIDATION PANEL ============== function Validation({ data, loading, onOpenRate, terminalFilter, onRefresh }) { const [severity, setSeverity] = useState(""); const [colFilters, setColFilters] = useState({}); const [openDropdown, setOpenDropdown] = useState(null); const [dropdownSearch, setDropdownSearch] = useState(""); const [fixing, setFixing] = useState(new Set()); // Set of target strings being fixed const [fixed, setFixed] = useState(new Set()); // Set of targets successfully fixed const handleAutoFix = async (r) => { const numId = parseInt(r.target.replace(/^R-/i, ""), 10); if (!numId) return; const detail = r.detail || ""; let updates = null; if (detail.includes("slab_max = 0")) updates = { slab_max: "" }; // "" → NULL in batch PATCH if (detail.includes("slab_min = 0")) updates = { slab_min: 1 }; if (!updates) return; setFixing(prev => new Set(prev).add(r.target)); try { await apiFetch("/api/rates/batch", { method: "PATCH", body: JSON.stringify({ ids: [numId], updates }), }); setFixed(prev => new Set(prev).add(r.target)); onRefresh?.(); } catch { /* leave button in error state — user can retry */ } setFixing(prev => { const s = new Set(prev); s.delete(r.target); return s; }); }; const findings = (data?.findings || []).filter(r => !terminalFilter || r.terminal === terminalFilter); const rules = data?.rules || []; const errCount = findings.filter(r => r.severity === "Error").length; const warnCount = findings.filter(r => r.severity === "Warning").length; // severity button filter first, then column filters const severityRows = findings.filter(r => !severity || r.severity === severity); const rows = severityRows.filter(r => Object.entries(colFilters).every(([k, sel]) => !sel.length || sel.includes(String(r[k] ?? ""))) ); const activeFilterCount = Object.values(colFilters).filter(v => v.length > 0).length; const lastRunStr = data?.last_run ? `${data.last_run} · ${(data.rates_scanned || 0).toLocaleString()} rates scanned` : "—"; useEffect(() => { const h = e => { if (!e.target.closest("[data-vfdrop]")) { setOpenDropdown(null); setDropdownSearch(""); } }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); // column filter dropdown — options sourced from severityRows (respects severity button) const fDrop = (key, header) => { const options = [...new Set(severityRows.map(r => String(r[key] ?? "")).filter(Boolean))].sort(); const sel = colFilters[key] || []; const active = sel.length > 0; const dropId = `v-${key}`; return (
{header}
e.stopPropagation()}> {openDropdown === dropId && (
setDropdownSearch(e.target.value)} placeholder="Search…" style={{ width: "100%", padding: "3px 7px", fontSize: 12, border: "1px solid var(--border)", borderRadius: 3, outline: "none", boxSizing: "border-box", background: "var(--paper)" }} />
{options.filter(v => !dropdownSearch || v.toLowerCase().includes(dropdownSearch.toLowerCase())).map(v => ( ))} {options.filter(v => !dropdownSearch || v.toLowerCase().includes(dropdownSearch.toLowerCase())).length === 0 && (
No options
)}
)}
); }; return (
{[ { v: "", l: `All ${findings.length}` }, { v: "Error", l: `Errors ${errCount}`, c: "var(--red)" }, { v: "Warning", l: `Warnings ${warnCount}`, c: "var(--amber)" }, ].map(s => ( ))}
Last run: {lastRunStr}
1 ? "s" : ""} active)` : ""}`} sub="Errors block save · warnings allow save with notice" right={activeFilterCount > 0 && ( )}>
{fDrop("rule", "Rule")} {fDrop("severity", "Severity")} {fDrop("terminal", "Terminal")} {fDrop("cost_type", "Cost Type")} {fDrop("shipment_type", "Shipment Type")} {fDrop("target", "Target")} {rows.map((r, i) => ( ))} {!loading && rows.length === 0 && ( )}
Detail
{r.rule}
{r.name}
{r.severity === "Error" && Error} {r.severity === "Warning" && Warning} {r.terminal} {r.cost_type || } {r.shipment_type || } {r.target} {r.detail} {r.rule === "QV-02" && ( )} {r.rule === "QV-06" && r.severity === "Warning" && (() => { const isFixing = fixing.has(r.target); const isFixed = fixed.has(r.target); return ( ); })()} {onOpenRate && r.target && ( )}
No findings{severity ? ` for ${severity}` : ""}{ activeFilterCount ? " matching filters" : ""}
{rules.map((r, i) => (
{r.id} {r.name} {r.severity === "Error" ? "Blocks save" : "Warns"}
{r.desc}
))}
); } const QV08_COLS = [ { key: "containertype", label: "Equipment Type" }, { key: "container_size", label: "Container Size" }, { key: "container_fullness", label: "Full/Empty" }, { key: "shipment_type", label: "Shipment Type" }, { key: "slab_type", label: "Slab Type" }, { key: "slab_unit", label: "Slab Unit" }, ]; function QV08Config({ data, perms, onSaved }) { const canEdit = perms?.edit || perms?.admin; // local draft state: { [cost_type_id]: { required_cols: string[], qv_severity: string, dirty: bool, saving: bool, saved: bool } } const [draft, setDraft] = useState({}); useEffect(() => { if (!data) return; const init = {}; data.forEach(ct => { init[ct.id] = { required_cols: ct.required_cols || [], qv_severity: ct.qv_severity || "Warning", dirty: false, saving: false, saved: false, }; }); setDraft(init); }, [data]); const toggleCol = (id, col) => { if (!canEdit) return; setDraft(prev => { const cur = prev[id] || { required_cols: [], qv_severity: "Warning" }; const cols = cur.required_cols.includes(col) ? cur.required_cols.filter(c => c !== col) : [...cur.required_cols, col]; return { ...prev, [id]: { ...cur, required_cols: cols, dirty: true, saved: false } }; }); }; const setSeverity = (id, val) => { if (!canEdit) return; setDraft(prev => { const cur = prev[id] || { required_cols: [], qv_severity: "Warning" }; return { ...prev, [id]: { ...cur, qv_severity: val, dirty: true, saved: false } }; }); }; const saveRow = async (id) => { const row = draft[id]; if (!row) return; setDraft(prev => ({ ...prev, [id]: { ...prev[id], saving: true } })); try { await apiFetch(`/api/health/config/required-cols/${id}`, { method: "PUT", body: JSON.stringify({ required_cols: row.required_cols, qv_severity: row.qv_severity }), }); setDraft(prev => ({ ...prev, [id]: { ...prev[id], saving: false, dirty: false, saved: true } })); onSaved?.(); } catch { setDraft(prev => ({ ...prev, [id]: { ...prev[id], saving: false } })); } }; if (!data) { return (
Loading…
); } const rows = (data || []).filter(ct => ct.is_active); return ( {!canEdit && (
Read-only — contact an Admin to change these settings.
)}
{QV08_COLS.map(c => ( ))} {canEdit && } {rows.map(ct => { const d = draft[ct.id] || { required_cols: ct.required_cols || [], qv_severity: ct.qv_severity || "Warning" }; return ( {QV08_COLS.map(col => { const checked = d.required_cols.includes(col.key); return ( ); })} {canEdit && ( )} ); })} {rows.length === 0 && ( )}
Cost Type {c.label} If Missing
{ct.name}
{ct.code}
toggleCol(ct.id, col.key)} style={{ width: 16, height: 16, cursor: canEdit ? "pointer" : "default", accentColor: "var(--teal)" }} /> {canEdit ? ( ) : ( {d.qv_severity} )} {d.saving ? (
) : d.saved && !d.dirty ? ( ) : ( )}
No active cost types configured.
); } Object.assign(window, { RateHealth });