// GUDID match approval view - Phase B.2.6 UI surface. const { useEffect: useEffectGudid, useMemo: useMemoGudid, useState: useStateGudid, useRef: useRefGudid } = React; const ADD_TO_LIBRARY_TOOLTIP = "GUDID covers about 30% of OR supplies. Items here are normal - flag them to expand the catalog."; const GUDID_EMBEDDED_APP_SHELL = new URLSearchParams(window.location.search).get("embed") === "shell"; // Live card header helpers: function firstLiveCardText(...values) { for (const value of values) { const text = String(value || "").replace(/\s+/g, " ").trim(); if (text) return text; } return ""; } function cardWithLiveHeader(card = {}) { const live = window.PREFCARD_LIVE_CARD || {}; return { ...card, name: firstLiveCardText(live.procedure_name, card.name), maintainer: firstLiveCardText(live.surgeon_name, card.maintainer, "Maintainer unknown"), maintainerRole: firstLiveCardText(live.surgeon_specialty, live.surgeon_title, card.maintainerRole), maintainerInst: firstLiveCardText(live.facility_name, card.maintainerInst), lastEdited: firstLiveCardText(String(live.last_updated || "").slice(0, 10), card.lastEdited), }; } // Live card header helpers end. const MATCH_COPY = { block: { label: "Add to library", detail: "Catalog expansion needed", fg: "var(--ss-match-reject)", bg: "var(--ss-match-reject-bg)", bd: "#DAB5AC", icon: "alert", }, reject: { label: "Reject", detail: "Manual catalog entry required", fg: "var(--ss-match-reject)", bg: "var(--ss-match-reject-bg)", bd: "#DAB5AC", icon: "alert", }, no_match: { label: "No match", detail: "Add to library", fg: "var(--ss-match-nomatch)", bg: "var(--ss-match-nomatch-bg)", bd: "#D6D0C5", icon: "x", }, review: { label: "Amber review", detail: "Verify before save", fg: "var(--ss-match-review)", bg: "var(--ss-match-review-bg)", bd: "#DEC2B2", icon: "review", }, auto: { label: "Confirm badge", detail: "Auto-accept", fg: "var(--ss-match-auto)", bg: "var(--ss-match-auto-bg)", bd: "#B9D0BE", icon: "check", }, auto_pending: { label: "Confirm badge", detail: "Auto-pending UI confirm", fg: "var(--ss-match-auto)", bg: "var(--ss-match-auto-bg)", bd: "#B9D0BE", icon: "check", }, verified: { label: "GUDID verified", detail: "High confidence match", fg: "var(--ss-match-verified)", bg: "var(--ss-match-verified-bg)", bd: "#B9D0BE", icon: "check", }, }; const GUDID_ROOM_DEFAULT_RULES = [ { id: "esu", label: "ESU", defaultLabel: "ESU assumed in room", accessoryPattern: /\b(bovie|cautery|electrosurgical)\b.*\b(pencil|tip|cord|pad|blade)\b|\b(pencil|tip)\b.*\b(bovie|cautery|electrosurgical)\b/i, equipmentPattern: /\b(esu|electrosurgical unit|cautery generator|bovie generator|valleylab|force triad)\b/i, targetLabels: ["boom", "equipment cart", "Bovie tower"], bodyTemplate: (sourceName) => `This item (${sourceName}) is an ESU accessory, but no electrosurgical unit is listed on the card. The ESU is assumed to be a room default.`, }, { id: "suction", label: "Suction", defaultLabel: "Suction assumed in room", accessoryPattern: /\b(yankauer|poole|suction tubing|suction tip|suction canister|suction liner)\b/i, equipmentPattern: /\b(suction machine|suction pump|suction tower|suction regulator|wall suction)\b/i, targetLabels: ["boom", "equipment cart", "suction tower"], bodyTemplate: (sourceName) => `This item (${sourceName}) is a suction accessory, but no suction equipment is listed on the card. Suction is assumed to be a room default.`, }, ]; function itemSearchText(item) { return [item?.item_name, item?.name, item?.raw_text].filter(Boolean).join(" ").toLowerCase(); } function itemDisplayName(item) { return item?.item_name || item?.name || item?.raw_text || "this item"; } function itemRoomDefault(item, cardItems) { if (!item || !Array.isArray(cardItems)) return null; const itemText = itemSearchText(item); for (const rule of GUDID_ROOM_DEFAULT_RULES) { if (!rule.accessoryPattern.test(itemText)) continue; const hasExplicitEquipment = cardItems.some((other) => other !== item && rule.equipmentPattern.test(itemSearchText(other)) ); if (hasExplicitEquipment) continue; const sourceName = itemDisplayName(item); return { rule, sourceName, body: rule.bodyTemplate(sourceName) }; } return null; } function getGudidBand(item) { const match = item.gudid_match; if (!match) return "no_match"; // Scanner-bound items have gudid_match populated from FDA GUDID lookup but // never ran through the text-matcher, so confidence is null. Trust the // barcode binding (user explicitly scanned) and treat as verified. Anchored // in Ada's PR #1164 edge-case 2026-05-22T~17:35Z. if (match.match_confidence == null) return "verified"; if (match.band) return match.band; if (match.match_confidence < 0.5) return "block"; if (match.match_confidence < 0.85) return "review"; if (match.match_confidence < 0.9) return "auto_pending"; return "verified"; } function percent(value) { if (typeof value !== "number") return "0%"; return `${Math.round(value * 100)}%`; } function prettifyPath(path) { return (path || "no_match").replaceAll("_", " "); } // CAT grouping helpers: keep this block JSX-free so node tests can eval it. const PREFCARD_SECTION_ORDER = [ "prep", "position", "drapes", "sterile_supplies", "instruments", "rep_trays", "equipment", "meds", "suture", "dressing", "hold", "notes", ]; const LEGACY_PHASE_SECTION = { setup: "sterile_supplies", incision: "instruments", close: "dressing" }; const CAT_COLLAPSE_THRESHOLD = 5; function labelForCardSection(section) { return (section || "notes").replaceAll("_", " ").replace(/\b\w/g, (char) => char.toUpperCase()); } function quantityValue(value) { const qty = Number(value); return Number.isFinite(qty) ? qty : 0; } function sectionMapFromCardPayload(payload) { const byId = new Map(); for (const [section, sectionItems] of Object.entries(payload?.sections || {})) { if (!Array.isArray(sectionItems)) continue; for (const item of sectionItems) { if (item?.id == null) continue; byId.set(String(item.id), section); byId.set(`card-item-${item.id}`, section); } } return byId; } function sectionKeyForItem(item, sectionMap = new Map()) { const mapped = sectionMap.get(String(item?.card_item_id)) || sectionMap.get(String(item?.id)); const raw = mapped || item?.section_key || item?.section_enum || item?.card_section || item?.section || "notes"; if (PREFCARD_SECTION_ORDER.includes(raw)) return raw; return LEGACY_PHASE_SECTION[raw] || "notes"; } function itemAppearsOpen(item) { const open = quantityValue(item?.qty_open); const hold = quantityValue(item?.qty_hold); return open > 0 || open + hold === 0; } function itemAppearsHold(item) { return quantityValue(item?.qty_hold) > 0; } function sortSectionKeys(a, b) { const ai = PREFCARD_SECTION_ORDER.indexOf(a); const bi = PREFCARD_SECTION_ORDER.indexOf(b); return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi) || a.localeCompare(b); } function buildCategoryGroups(items, cardPayload) { const sectionMap = sectionMapFromCardPayload(cardPayload); const buckets = { open: new Map(), hold: new Map() }; for (const item of items || []) { const section = sectionKeyForItem(item, sectionMap); const targets = []; if (itemAppearsOpen(item)) targets.push("open"); if (itemAppearsHold(item)) targets.push("hold"); for (const target of targets) { if (!buckets[target].has(section)) buckets[target].set(section, []); buckets[target].get(section).push(item); } } const finalize = (map) => ({ total: Array.from(map.values()).reduce((sum, sectionItems) => sum + sectionItems.length, 0), sections: Array.from(map.keys()).sort(sortSectionKeys).map((section) => ({ key: section, label: labelForCardSection(section), items: map.get(section), defaultCollapsed: map.get(section).length > CAT_COLLAPSE_THRESHOLD, })), }); return { open: finalize(buckets.open), hold: finalize(buckets.hold) }; } // CAT grouping helpers end. function GudidMatchApprovalView({ mobile = false, viewTabs = null }) { // Live source built from real /api/cards/{id} payload (PR #1164 + post-PR // projection in data.js::mapApiToGudidMatchLive). The companion // PREFCARD_GUDID_MATCH_RESPONSE is retained for view-primitive.jsx's // design-system band illustration only. const [cardSnapshot, setCardSnapshot] = useStateGudid(window.PREFCARD); const [liveResponse, setLiveResponse] = useStateGudid(window.PREFCARD_GUDID_MATCH_LIVE || window.PREFCARD_GUDID_MATCH_RESPONSE); const [selectedFiles, setSelectedFiles] = useStateGudid([]); const [uploadResult, setUploadResult] = useStateGudid(window.PREFCARD_UPLOAD_RESULT || null); const [uploadState, setUploadState] = useStateGudid({ phase: "idle", message: "" }); const [approveState, setApproveState] = useStateGudid({ phase: "idle", message: "" }); const response = liveResponse || window.PREFCARD_GUDID_MATCH_RESPONSE; const items = response.matched_items || []; const [filter, setFilter] = useStateGudid("all"); const [detailItem, setDetailItem] = useStateGudid(null); const [verifiedIds, setVerifiedIds] = useStateGudid(new Set()); const [manualNames, setManualNames] = useStateGudid({}); const [driftChoice, setDriftChoice] = useStateGudid({}); const [activeQtyTab, setActiveQtyTab] = useStateGudid("open"); const stats = useMemoGudid(() => summarizeGudidItems(items), [items]); const visibleItems = useMemoGudid(() => items.filter((item) => { const band = getGudidBand(item); if (filter === "manual") return band === "block" || band === "reject" || band === "no_match"; if (filter === "review") return band === "review"; if (filter === "auto") return band === "auto" || band === "auto_pending" || band === "verified"; return true; }), [items, filter]); const categoryGroups = useMemoGudid( () => buildCategoryGroups(visibleItems, window.PREFCARD_LAST_PAYLOAD), [visibleItems, cardSnapshot] ); const headerCard = cardWithLiveHeader(cardSnapshot); function verifyItem(id) { setVerifiedIds((prev) => { const next = new Set(prev); next.add(id); return next; }); } function setManualName(id, value) { setManualNames((prev) => ({ ...prev, [id]: value })); } async function matchUploadedItems(result) { const extractionItems = result?.extraction?.items || []; const supplyItems = extractionItems .filter((item) => item.section !== "notes" && (item.item_name || item.raw_text)) .slice(0, 200); if (!supplyItems.length) return null; const response = await fetch(window.prefcardApiPath("/api/match-items"), { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ items: supplyItems.map((item) => ({ item_name: item.item_name || item.raw_text, catalog_num: item.catalog_num || null, vendor: item.vendor || null, })), require_active_distribution: true, }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function uploadSelectedFiles() { if (!selectedFiles.length) return; setUploadState({ phase: "uploading", message: `Combining ${selectedFiles.length} page${selectedFiles.length === 1 ? "" : "s"} into one preference card.`, }); setApproveState({ phase: "idle", message: "" }); const form = new FormData(); selectedFiles.forEach((file) => form.append("files", file)); form.append("source_type", "prefcard_regen_canvas"); try { const response = await fetch(window.prefcardApiPath("/api/upload/card"), { method: "POST", credentials: "same-origin", body: form, }); const result = await response.json(); if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`); let matchResponse = null; try { matchResponse = await matchUploadedItems(result); } catch (matchErr) { console.warn("[prefcard-multi-view] match-items probe failed; showing upload extraction only:", matchErr); } window.applyUploadResult(result, matchResponse); setCardSnapshot(window.PREFCARD); setLiveResponse(window.PREFCARD_GUDID_MATCH_LIVE); setUploadResult(result); setFilter("all"); setUploadState({ phase: "done", message: `Merged ${result.page_count || result.extraction?.page_count || selectedFiles.length} page${selectedFiles.length === 1 ? "" : "s"}; ${result.items_count || result.extraction?.items?.length || 0} extracted rows ready for review.`, }); } catch (err) { setUploadState({ phase: "error", message: err.message || "Upload failed" }); } } async function approveUpload() { const uploadId = uploadResult?.upload_id || cardSnapshot.uploadId; if (!uploadId) return; setApproveState({ phase: "saving", message: "Approving merged extraction." }); try { const response = await fetch(window.prefcardApiPath(`/api/upload/card/${uploadId}/approve`), { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ items: uploadResult?.extraction?.items || [], }), }); const result = await response.json(); if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`); if (result.card_id) { const cardResponse = await fetch(window.prefcardApiPath(`/api/cards/${result.card_id}`), { credentials: "same-origin", headers: { "Accept": "application/json" }, }); if (cardResponse.ok) { window.applyPrefcardPayload(await cardResponse.json()); setCardSnapshot(window.PREFCARD); setLiveResponse(window.PREFCARD_GUDID_MATCH_LIVE); } } setApproveState({ phase: "done", message: `${result.items_added || 0} rows approved; ${result.items_needs_review || 0} need human review.`, }); } catch (err) { setApproveState({ phase: "error", message: err.message || "Approve failed" }); } } function applyScanResolved(item, result) { const nextMatch = { primary_di: result.gudid_di, // Precedence: most-specific first. gudid_description carries the full canonical // product (e.g. "Stryker Performance Series Sagittal Blade") while gudid_brand // is a generic family label (e.g. "Stryker Performance Series"). Surgeon's // item_name preserves their familiar text + size + catalog# context — second // choice when GUDID description is missing. gudid_brand is last-resort fallback. canonical_name: result.gudid_description || item.item_name || result.gudid_brand || "-", brand_name: result.gudid_brand || "-", company_name: result.gudid_company || "-", device_description: result.gudid_description || "", match_confidence: result.match_confidence ?? 1, match_path: result.match_path || "scan", match_pin_source: "scan_barcode", band: "verified", }; setLiveResponse((prev) => ({ ...prev, matched_items: (prev?.matched_items || []).map((row) => ( row.id === item.id || row.card_item_id === item.card_item_id ? { ...row, needs_human_review: 0, gudid_match: nextMatch } : row )), })); setDetailItem((prev) => (prev ? { ...prev, needs_human_review: 0, gudid_match: nextMatch } : prev)); } return (
{!GUDID_EMBEDDED_APP_SHELL && (mobile ? : )} {viewTabs}
setDriftChoice((prev) => ({ ...prev, [id]: value }))} /> {!mobile && ( getGudidBand(item) === "review")} onDetail={setDetailItem} /> )}
{detailItem && ( setDetailItem(null)} onScanResolved={applyScanResolved} /> )}
); } function GudidMobileHeader({ card }) { const maintainer = card?.maintainer || "Maintainer"; return (
ScrubStack
Prefcards
); } function GudidUploadPanel({ mobile, files, uploadState, approveState, uploadResult, onFiles, onUpload, onApprove, }) { const pageCount = uploadResult?.page_count || uploadResult?.extraction?.page_count; const itemCount = uploadResult?.items_count || uploadResult?.extraction?.items?.length; const busy = uploadState.phase === "uploading" || approveState.phase === "saving"; return (
Multi-page upload
onFiles(Array.from(event.target.files || []))} style={{ maxWidth: mobile ? "100%" : 260, fontFamily: "var(--ss-sans)", fontSize: 12.5, color: "var(--ss-gray-900)", }} /> {(uploadResult || uploadState.phase === "done") && ( )}
{files.length > 0 && (
{files.map((file, index) => (
Page {index + 1}
{file.name}
))}
)}
{(uploadState.message || approveState.message) && (
{approveState.message || uploadState.message}
)}
); } function GudidNotesPanel({ card, mobile }) { const historyNotes = card.historyNotes || []; const clinicalNotes = card.clinicalNotes || []; if (!historyNotes.length && !clinicalNotes.length) return null; return (
{historyNotes.length > 0 && ( {historyNotes.map((note) => (
  • {note.note_text}
  • ))}
    )} {clinicalNotes.length > 0 && ( {clinicalNotes.map((note) => (
  • {note.item_name}
    {note.raw_text || "(none captured)"}
  • ))}
    )}
    ); } function NoteBucket({ title, count, children, mobile }) { return (
    {title} {count}
    ); } function summarizeGudidItems(items) { return items.reduce((acc, item) => { const band = getGudidBand(item); acc.total += 1; if (band === "block" || band === "reject" || band === "no_match") acc.manual += 1; if (band === "review") acc.review += 1; if (band === "auto" || band === "auto_pending" || band === "verified") acc.auto += 1; if (item.gudid_match) acc.matched += 1; return acc; }, { total: 0, matched: 0, auto: 0, review: 0, manual: 0, noMatch: 0 }); } function GudidPageHeader({ mobile, card, stats, pendingUploadId, approveState, onApprove }) { const canApprove = Boolean(pendingUploadId) && approveState.phase !== "saving"; return (
    Prefcard regen approval

    {card.name} with GUDID match review

    {card.maintainer}
    {card.maintainerInst}
    {pendingUploadId ? `upload ${pendingUploadId}` : `card ${window.PREFCARD_CONFIG.cardId}`}
    ); } function HeaderMetric({ label, value }) { return (
    {label}
    {value}
    ); } function GudidStatsBar({ stats, filter, onFilter, mobile }) { const segments = [ { id: "all", label: "Total", value: stats.total, tone: "neutral" }, { id: "auto", label: "Auto-accept", value: stats.auto, tone: "verified" }, { id: "review", label: "Amber review", value: stats.review, tone: "review" }, { id: "manual", label: "Add to library", value: stats.manual, tone: "reject", title: ADD_TO_LIBRARY_TOOLTIP }, ]; return (
    {segments.map((segment) => ( ))}
    ); } function statButtonTone(tone) { if (tone === "verified") return { color: "var(--ss-match-verified)", background: "var(--ss-match-verified-bg)" }; if (tone === "review") return { color: "var(--ss-match-review)", background: "var(--ss-match-review-bg)" }; if (tone === "reject") return { color: "var(--ss-match-reject)", background: "var(--ss-match-reject-bg)" }; return { color: "var(--ss-gray-900)", background: "var(--ss-cream)" }; } function GudidCategorizedRows({ groups, activeTab, cardId, mobile, verifiedIds, manualNames, driftChoice, onTab, onManualName, onVerify, onDetail, onDriftChoice, }) { const activeGroup = groups[activeTab] || groups.open; const tabs = [ { id: "open", label: "Open", group: groups.open }, { id: "hold", label: "Hold", group: groups.hold }, ]; return (
    {tabs.map((tab) => ( ))}
    {activeGroup.sections.length ? activeGroup.sections.map((section) => ( )) : (
    No {activeTab} items match the current GUDID filter.
    )}
    ); } function GudidSectionAccordion({ section, cardId, mobile, verifiedIds, manualNames, driftChoice, onManualName, onVerify, onDetail, onDriftChoice, }) { const storageKey = `prefcard-cat:${cardId}:${section.key}`; const [collapsed, setCollapsed] = useStateGudid(() => { try { const saved = window.sessionStorage.getItem(storageKey); if (saved != null) return saved === "1"; } catch (_) {} return section.defaultCollapsed; }); useEffectGudid(() => { try { const saved = window.sessionStorage.getItem(storageKey); setCollapsed(saved != null ? saved === "1" : section.defaultCollapsed); } catch (_) { setCollapsed(section.defaultCollapsed); } }, [storageKey, section.defaultCollapsed]); function toggle() { setCollapsed((prev) => { const next = !prev; try { window.sessionStorage.setItem(storageKey, next ? "1" : "0"); } catch (_) {} return next; }); } return (
    {!collapsed && (
    {section.items.map((item) => ( ))}
    )}
    ); } function GudidMatchRow({ item, sectionKey, mobile, verified, manualName, driftChoice, onManualName, onVerify, onDetail, onDriftChoice, }) { const band = getGudidBand(item); const match = item.gudid_match; const meta = MATCH_COPY[band]; const needsManual = band === "block" || band === "reject" || band === "no_match"; const needsVerify = band === "review" && !verified; return (
    {mobile && }
    {match && } qty {item.qty_open} open / {item.qty_hold} hold
    {item.notes && (
    {item.notes}
    )} {band === "review" && ( )} {needsManual && ( )} {match?.drift_detected && ( )}
    {!mobile && } {cardItemIdForProductInfo(item) && } {needsVerify && } {verified && verified}
    ); } function SectionChip({ section }) { return ( {labelForCardSection(section)} ); } function supplyGlyphSeed(item) { const kind = item.section === "incision" ? "implant" : item.section === "prep" ? "equipment" : "consumable"; return { id: item.id, kind }; } function MatchConfidenceChip({ item, verified = false, onOpen, compact = false }) { const rawBand = getGudidBand(item); const band = verified ? "verified" : rawBand; const match = item.gudid_match; const meta = MATCH_COPY[band]; const silentVerified = rawBand === "verified" && !verified; const label = band === "block" || band === "reject" ? `Reject ${percent(match.match_confidence)}` : band === "no_match" ? "No match" : band === "verified" ? (match && match.match_confidence == null ? "Scanner" : "Verified") : `${band === "review" ? "Amber" : "Confirm"} ${percent(match.match_confidence)}`; return ( ); } function CanonicalNameBlock({ item, manualName }) { const band = getGudidBand(item); const match = item.gudid_match; const showDiff = band === "review"; const title = band === "block" || band === "reject" || band === "no_match" ? (manualName || item.item_name) : match.canonical_name; return (
    {showDiff && (
    {item.item_name}
    )}

    {title}

    {match ? `${match.brand_name} / ${match.company_name}` : `${item.vendor || "Unknown vendor"} / no GUDID match`}
    ); } function DiffPanel({ item, verified }) { const match = item.gudid_match; return (
    Vision {item.item_name}
    GUDID {match.canonical_name}
    {percent(match.match_confidence)} through {prettifyPath(match.match_path)}
    ); } function ManualEntryPanel({ item, value, onChange }) { return (
    onChange(item.id, event.target.value)} placeholder="Manual canonical name" style={{ minWidth: 0, border: "1px solid #DAB5AC", borderRadius: 4, padding: "7px 9px", fontFamily: "var(--ss-sans)", fontSize: 12.5, background: "var(--ss-warm-white)", color: "var(--ss-ink)", }} />
    {item.gudid_match ? "Rejected below confidence threshold; manual catalog entry required." : "No GUDID match returned; add manually before approval."}
    ); } function DriftPanel({ item, choice, onChoice }) { const match = item.gudid_match; return (
    Cerner card has {match.canonical_name}, discontinued {match.distribution_end_date}. Active successor: {match.successor_pin}.
    ); } function GudidSidePanel({ stats, selected, onDetail }) { return ( ); } function MiniStat({ label, value }) { return (
    {value}
    {label}
    ); } function GudidDetailModal({ item, cardItems = [], onClose, onScanResolved }) { const [productInfo, setProductInfo] = useStateGudid({ phase: "idle", enrichment: null, imageUrl: null, error: "" }); const [scanOpen, setScanOpen] = useStateGudid(false); const cardItemId = cardItemIdForProductInfo(item); useEffectGudid(() => { let cancelled = false; if (!cardItemId) { setProductInfo({ phase: "unavailable", enrichment: null, imageUrl: null, error: "" }); return () => { cancelled = true; }; } setProductInfo({ phase: "loading", enrichment: null, imageUrl: null, error: "" }); Promise.all([ fetch(window.prefcardApiPath(`/api/items/${cardItemId}/enrich`), { method: "POST", credentials: "same-origin", headers: { "Accept": "application/json" }, }).then(async (response) => { const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload.detail || `HTTP ${response.status}`); return payload; }), fetch(window.prefcardApiPath(`/api/product-image/${cardItemId}`), { credentials: "same-origin", headers: { "Accept": "application/json" }, }).then((response) => response.ok ? response.json() : { image_url: null }).catch(() => ({ image_url: null })), ]).then(([enrichment, image]) => { if (cancelled) return; setProductInfo({ phase: "ready", enrichment, imageUrl: image?.image_url || null, error: "" }); }).catch((err) => { if (cancelled) return; setProductInfo({ phase: "error", enrichment: null, imageUrl: null, error: err.message || "Lookup failed" }); }); return () => { cancelled = true; }; }, [cardItemId]); return (
    { if (event.target === event.currentTarget) onClose(); }} style={{ position: "fixed", inset: 0, zIndex: 20, background: "rgba(27,26,24,0.28)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, }} >
    {}} /> {cardItemId && }
    {scanOpen && ( setScanOpen(false)} onResolved={(result) => { onScanResolved?.(item, result); }} /> )}
    {cardItemId && }
    ); } function ScanBarcodeModal({ item, cardItemId, onClose, onResolved }) { const [barcode, setBarcode] = useStateGudid(""); const [barcodeType, setBarcodeType] = useStateGudid("unknown"); const [phase, setPhase] = useStateGudid("idle"); const [message, setMessage] = useStateGudid(""); const [fileName, setFileName] = useStateGudid(""); const [cameraOn, setCameraOn] = useStateGudid(false); const videoRef = useRefGudid(null); const streamRef = useRefGudid(null); const detectorSupported = typeof window !== "undefined" && "BarcodeDetector" in window; const cardId = window.PREFCARD_CONFIG?.cardId; async function resolveBarcodeValue(value, type) { const clean = (value || "").trim(); if (!clean) return; try { setPhase("resolving"); const response = await fetch(window.prefcardApiPath(`/api/cards/${cardId}/items/${cardItemId}/scan-barcode`), { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ barcode_value: clean, barcode_type: type || "unknown", raw_item_name: item.item_name || item.raw_text || "", }), }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`); if (!result.resolved) { setPhase("not-found"); setMessage(result.reason || "No GUDID record found for that barcode."); return; } setPhase("success"); setMessage(`Linked ${result.gudid_brand || "GUDID device"} and saved this facility mapping.`); onResolved?.(result); } catch (err) { setPhase("error"); setMessage(err.message || "Barcode scan failed."); } } useEffectGudid(() => { if (!cameraOn || !detectorSupported) return undefined; let cancelled = false; let rafId = null; let detector = null; (async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }); if (cancelled) { stream.getTracks().forEach((t) => t.stop()); return; } streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play().catch(() => {}); } detector = new window.BarcodeDetector({ formats: ["data_matrix", "qr_code", "ean_13", "code_128", "upc_a"] }); setPhase("scanning"); setMessage("Point camera at the barcode. Auto-detects once visible."); const scanFrame = async () => { if (cancelled || !videoRef.current || videoRef.current.readyState < 2) { if (!cancelled) rafId = requestAnimationFrame(scanFrame); return; } try { const codes = await detector.detect(videoRef.current); // PREFER DATA MATRIX over 1D when both are visible: GS1 DataMatrix carries // primary DI + lot + expiry that matches GUDID DB; 1D Code 128 often only // carries the GTIN with AIM identifier prefix that needs server-side parsing. const dataMatrix = codes && codes.find && codes.find((c) => c.format === "data_matrix"); const first = dataMatrix || (codes && codes[0]); if (first && first.rawValue) { setBarcode(first.rawValue); setBarcodeType(first.format || "live"); setMessage(`Detected ${first.format || "barcode"} — resolving...`); setCameraOn(false); await resolveBarcodeValue(first.rawValue, first.format || "live"); return; } } catch (e) { // transient frame error — keep scanning } if (!cancelled) rafId = requestAnimationFrame(scanFrame); }; scanFrame(); } catch (err) { setPhase("error"); setMessage(err.message || "Camera unavailable. Use file upload or type the code."); setCameraOn(false); } })(); return () => { cancelled = true; if (rafId) cancelAnimationFrame(rafId); if (streamRef.current) { streamRef.current.getTracks().forEach((t) => t.stop()); streamRef.current = null; } }; }, [cameraOn, detectorSupported]); async function detectFromFile(event) { const file = event.target.files?.[0]; if (!file) return; setFileName(file.name); setMessage(""); if (!detectorSupported) { setPhase("fallback"); setMessage("This browser cannot decode uploaded barcode images; enter the barcode printed on the package."); return; } try { setPhase("scanning"); const bitmap = await createImageBitmap(file); const detector = new window.BarcodeDetector({ formats: ["data_matrix", "qr_code", "ean_13", "code_128", "upc_a"] }); const codes = await detector.detect(bitmap); const first = codes?.[0]; if (!first) { setPhase("idle"); setMessage("No barcode detected in that image."); return; } setBarcode(first.rawValue || ""); setBarcodeType(first.format || "image"); setPhase("ready"); setMessage(`Detected ${first.format || "barcode"} from ${file.name}.`); } catch (err) { setPhase("error"); setMessage(err.message || "Could not decode that image."); } } async function resolveBarcode() { const clean = barcode.trim(); if (!clean) { setPhase("error"); setMessage("Enter or upload a barcode first."); return; } try { setPhase("resolving"); const response = await fetch(window.prefcardApiPath(`/api/cards/${cardId}/items/${cardItemId}/scan-barcode`), { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ barcode_value: clean, barcode_type: barcodeType, raw_item_name: item.item_name || item.raw_text || "", }), }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`); if (!result.resolved) { setPhase("not-found"); setMessage(result.reason || "No GUDID record found for that barcode."); return; } setPhase("success"); setMessage(`Linked ${result.gudid_brand || "GUDID device"} and saved this facility mapping.`); onResolved?.(result); } catch (err) { setPhase("error"); setMessage(err.message || "Barcode scan failed."); } } return (
    Scan to learn

    Bind barcode to this item

    Future cards at this facility will try this scanned GUDID match before generic text matching.
    {detectorSupported && (
    {cameraOn && (
    )} {!detectorSupported && (
    Live camera barcode scan requires BarcodeDetector API (Chrome/Edge on Android). On unsupported browsers, use file upload or type the code below.
    )} {fileName &&
    {fileName}
    } {message && (
    {message}
    )}
    ); } function cardItemIdForProductInfo(item) { const raw = item?.card_item_id || item?.backend_id; if (raw && Number.isFinite(Number(raw))) return Number(raw); // Fallback: items from /api/cards/{id} responses have item.id = card_items.id (numeric). // The /api/match-items preview path uses synthesized string ids — caller falls through to the regex below. if (item?.id && Number.isFinite(Number(item.id))) return Number(item.id); const match = String(item?.id || "").match(/card-item-(\d+)$/); return match ? Number(match[1]) : null; } function ProductInfoLookupPanel({ item, cardItems = [], lookup }) { if (lookup.phase === "loading" || lookup.phase === "idle") { return (
    Looking up product...
    ); } if (lookup.phase === "unavailable") { return (
    Saved item needed This row is not persisted yet, so live openFDA and product-image lookup are unavailable.
    ); } if (lookup.phase === "error") { return (
    Lookup unavailable {lookup.error}
    ); } const enrichment = lookup.enrichment || {}; const match = item.gudid_match; const marketplace = enrichment.marketplace_match || {}; const safety = enrichment.safety || {}; const rep = enrichment.surgx_rep; const manufacturer = enrichment.company || marketplace.manufacturer_name || match?.company_name || item.vendor || ""; const imageUrl = lookup.imageUrl; const roomDefault = itemRoomDefault(item, cardItems); return (
    {(enrichment.description || marketplace.device_description || match?.device_description) && (
    Description
    {enrichment.description || marketplace.device_description || match?.device_description}
    )} {!item.gudid_match && }
    ); } function disruptionPrimaryDi(item) { return item?.gudid_di || item?.gudid_match?.primary_di || item?.gudid_match?.di || ""; } function disruptionTierStyle(severity) { const tier = String(severity || "low").toLowerCase(); if (tier === "critical") { return { background: "var(--ss-match-reject-bg)", border: "#DAB5AC", color: "var(--ss-match-reject)" }; } if (tier === "moderate") { return { background: "var(--ss-open-bg)", border: "#E1CC9E", color: "#7A5615" }; } return { background: "var(--ss-cream)", border: "var(--ss-line-soft)", color: "var(--ss-gray-700)" }; } function disruptionTopAlert(alerts) { const rank = { critical: 3, moderate: 2, low: 1 }; return [...alerts].sort((left, right) => { const rightRank = rank[String(right.severity || "low").toLowerCase()] || 0; const leftRank = rank[String(left.severity || "low").toLowerCase()] || 0; return rightRank - leftRank; })[0]; } function disruptionFirmLabel(alerts, topAlert) { const firms = [...new Set(alerts.map((alert) => alert.recalling_firm).filter(Boolean))]; if (firms.length > 1) return "Multiple manufacturers"; return firms[0] || topAlert?.recalling_firm || "Manufacturer not listed"; } function substitutesForItem(substitutesList, itemName) { if (!Array.isArray(substitutesList) || !substitutesList.length) return { alternatives: [], label: "" }; const target = String(itemName || "").trim().toLowerCase(); const exact = target ? substitutesList.find((group) => String(group.for_item || "").trim().toLowerCase() === target) : null; const group = exact || substitutesList[0] || {}; return { alternatives: Array.isArray(group.alternatives) ? group.alternatives : [], label: group.for_item || itemName || "", }; } function SubstitutesPanel({ state, itemName, onRetry, onMarkSubstitute, substitutedProductId, disabled = false }) { if (state.phase === "loading") { return (
    Looking up substitutes...
    ); } if (state.phase === "error") { return (
    Substitute lookup failed. Try again.
    ); } const { alternatives, label } = substitutesForItem(state.detail?.substitutes, itemName); if (!alternatives.length) { return (
    No substitute matches in catalog.
    Contact materials team for sourcing options.
    ); } return (
    Alternatives for {label || itemName}
    {alternatives.slice(0, 3).map((alt, idx) => { const isSubstituted = substitutedProductId != null && String(substitutedProductId) === String(alt.id); return (
    {alt.name || "Unnamed substitute"}
    {[alt.manufacturer, alt.vendor_name, alt.price != null && alt.price !== "" ? String(alt.price) : ""].filter(Boolean).join(" / ") || "Vendor details not listed"}
    {isSubstituted ? ( Substituted ) : ( )}
    ); })}
    ); } function SubstituteConfirmDialog({ itemName, substitute, onCancel, onConfirm }) { const [reason, setReason] = useStateGudid(""); const [phase, setPhase] = useStateGudid("idle"); const [error, setError] = useStateGudid(""); const dialogRef = React.useRef(null); const textareaRef = React.useRef(null); const cleanReason = reason.trim(); const canSubmit = cleanReason.length >= 50 && phase !== "saving"; useEffectGudid(() => { textareaRef.current?.focus(); }, []); const close = () => { if (phase !== "saving") onCancel(); }; const submit = async () => { if (!canSubmit) return; setPhase("saving"); setError(""); try { await onConfirm(cleanReason); setPhase("done"); } catch (err) { setPhase("idle"); setError(err.message || "Could not mark substitute. Try again."); } }; const onKeyDown = (event) => { if (event.key === "Escape") { event.preventDefault(); close(); return; } if (event.key !== "Tab" || !dialogRef.current) return; const focusable = Array.from(dialogRef.current.querySelectorAll("button, textarea")).filter((node) => !node.disabled); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }; return (
    { if (event.target === event.currentTarget) close(); }} style={substituteDialogOverlayStyle}>
    Mark as substituted
    This records that {substitute?.name || "Unnamed substitute"} is replacing {itemName || "this item"}.
    {substitute?.name || "Unnamed substitute"}
    {[substitute?.manufacturer, substitute?.vendor_name || substitute?.vendor].filter(Boolean).join(" / ") || "Vendor details not listed"}