let state; let groups = []; let selectedIndex = -1; let loadedIndex = -1; // which document the PDF viewer currently shows/loads let pdfViewer = null; let loadRequestId = 0; const people = document.querySelector("#people"); const fields = document.querySelector("#fields"); const viewer = document.querySelector(".viewer"); const pdfContainer = document.querySelector("#pdfViewer"); const pdfMessage = document.querySelector("#pdfMessage"); const status = document.querySelector("#status"); const search = document.querySelector("#search"); const errorBanner = document.querySelector("#errorBanner"); const fieldDefs = [ ["Name / Company", "name_company"], ["Prospective Buyer", "prospective_buyer"], ["Company", "company"], null, ["Phone", "phone"], ["Cell", "cell"], ["Email", "email"], null, ["Address", "address"], ["State", "state"], null, ["Businesses from Notes", "notes_business_raw"], ["Types of Businesses", "types_of_business_raw"], ["Background Experience", "background_experience"], null, ["How Did You Hear", "how_did_you_hear"], ["Interested in Updates", "interested_in_updates"], ["Down Payment", { key: "down_payment_raw", fallback: "down_payment" }], ["Total Purchase Price", "total_purchase_price"], ["Date of Introduction", "date_of_introduction"], null, ["Notes Page", "_notes_page"], ["Buyer Info Page", "_info_page"], ["CA Page", "_ca_page"], ]; const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[char]); function buildGroups(docs) { const map = new Map(); docs.forEach((doc, index) => { const key = doc.name_from_filename; const group = map.get(key) || { key, displayName: doc.prospective_buyer || key, docs: [], text: "", }; group.docs.push({ ...doc, index }); group.text += " " + [ key, doc.prospective_buyer, doc.types_of_business_raw, doc.notes_business_raw, doc.address, ].filter(Boolean).join(" "); map.set(key, group); }); return [...map.values()].sort((a, b) => a.key.localeCompare(b.key)); } function visibleGroups() { const terms = search.value.toLowerCase().trim().split(/\s+/).filter(Boolean); return groups.filter((group) => terms.every((term) => group.text.toLowerCase().includes(term)) ); } function updateStatus(shownCount = visibleGroups().length) { const source = state.dataSource === "sample" ? "sample data" : "JSON file"; status.textContent = `${shownCount} people / ${state.documents.length} documents \u00b7 ${source}`; status.className = ""; } function showError(message) { errorBanner.textContent = message; errorBanner.hidden = !message; } function renderList() { const shown = visibleGroups(); people.innerHTML = shown.map((group) => `
${ esc(group.displayName) } (${group.docs.length})
${ group.docs.map((doc) => ` `).join("") }
`).join(""); updateStatus(shown.length); } async function loadPdf(index) { const doc = state.documents[index]; if (!doc) return; // Deduplicate: don't reload the document that is already shown. // NOTE: must compare against loadedIndex, NOT selectedIndex -- // select() updates selectedIndex before calling loadPdf(), so a // selectedIndex comparison is always true and blocks every reload. if (index === loadedIndex && pdfViewer) { return; } loadedIndex = index; const previousZoom = pdfViewer ? pdfViewer.zoom : 1; if (pdfViewer) { pdfViewer.destroy(); } pdfViewer = new PdfViewer(pdfContainer, { initialZoom: previousZoom }); viewer.classList.remove("loaded"); pdfMessage.hidden = false; pdfMessage.textContent = "Loading PDF\u2026"; const reqId = "pdf-" + (++loadRequestId); console.log( `[BizMatch QC] PDF ${reqId} selection: ${doc._letter}/${doc.file_name}`, ); console.log(`[BizMatch QC] PDF ${reqId} prepare requested`); const startTime = performance.now(); try { const response = await fetch("/api/pdf/prepare", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ index, requestId: reqId }), }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || `Prepare failed: HTTP ${response.status}`); } const data = await response.json(); const prepareMs = Math.round(performance.now() - startTime); console.log( `[BizMatch QC] PDF ${reqId} prepare: ${prepareMs}ms, disk cache ${data.cacheStatus}`, ); if (index !== selectedIndex) { // A newer selection happened while preparing, don't show stale result return; } viewer.classList.add("loaded"); pdfMessage.hidden = true; console.log(`[BizMatch QC] PDF ${reqId} viewer load started`); await pdfViewer.load(data.url, data.size); console.log(`[BizMatch QC] PDF ${reqId} rendering complete`); } catch (error) { if (index !== selectedIndex) return; loadedIndex = -1; // allow retry: clicking the same document again reloads it const message = error instanceof Error ? error.message : String(error); pdfMessage.textContent = `Cannot open PDF: ${message}`; pdfMessage.hidden = false; } } function select(index) { selectedIndex = index; const doc = state.documents[index]; if (!doc) return; let rowIdx = 0; fields.innerHTML = `

${esc(doc.name_from_filename)}

${esc(doc.file_name)} \u00b7 ${ esc(doc._doc_type || "unknown") } \u00b7 ${esc(doc._pages_total ?? "?")} pages

${ doc._vision_error ? `

Vision error: ${esc(doc._vision_error)}

` : "" } ${ fieldDefs.map((def) => { if (!def) return '
'; const [label, keyOrObj] = def; let value; if (typeof keyOrObj === "object") { value = esc(doc[keyOrObj.key] || doc[keyOrObj.fallback] || "\u2014"); } else { value = esc(doc[keyOrObj] || "\u2014"); } const bgClass = rowIdx % 2 === 0 ? "row-even" : "row-odd"; rowIdx++; return `
${label}
${value}
`; }).join("") } `; void loadPdf(index); renderList(); } people.addEventListener("click", (event) => { const button = event.target.closest("[data-index]"); if (button) select(Number(button.dataset.index)); }); search.addEventListener("input", renderList); document.addEventListener("keydown", (event) => { if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return; if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); const delta = event.key === "ArrowDown" ? 1 : -1; select( Math.max( 0, Math.min( state.documents.length - 1, selectedIndex < 0 ? 0 : selectedIndex + delta, ), ), ); } }); const dialog = document.querySelector("#settingsDialog"); const jsonPath = document.querySelector("#jsonPath"); const pdfBase = document.querySelector("#pdfBase"); const useAnonData = document.querySelector("#useAnonData"); const settingsError = document.querySelector("#settingsError"); const saveSettings = document.querySelector("#saveSettings"); document.querySelector("#settings").onclick = () => { jsonPath.value = state.config.jsonPath || ""; pdfBase.value = state.config.pdfBaseDirectory || ""; useAnonData.checked = !!state.config.useAnonymousData; settingsError.hidden = true; settingsError.textContent = ""; dialog.showModal(); }; saveSettings.onclick = async (event) => { event.preventDefault(); saveSettings.disabled = true; settingsError.hidden = true; try { const response = await fetch("/api/config", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonPath: jsonPath.value, pdfBaseDirectory: pdfBase.value, useAnonymousData: useAnonData.checked, }), }); const body = await response.json(); if (!response.ok) throw new Error(body.error || "Could not save settings."); dialog.close(); await load(); } catch (error) { settingsError.textContent = error instanceof Error ? error.message : String(error); settingsError.hidden = false; } finally { saveSettings.disabled = false; } }; async function load() { try { const response = await fetch("/api/state", { cache: "no-store" }); if (!response.ok) { throw new Error(`State request failed with HTTP ${response.status}.`); } state = await response.json(); loadedIndex = -1; // document set changed; index-based dedup is invalid now groups = buildGroups(state.documents); showError(state.loadError || ""); renderList(); if (state.documents.length) select(0); } catch (error) { showError(error instanceof Error ? error.message : String(error)); } } void load();