/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // OldReports — read a packet of scanned / photographed old-DMS printouts and prepare it for import. // // 1. window.OldReports (the reading engine; no React) // readFile(file, { onProgress, rotate }) -> { id, name, kind, pages:[Page] } pdf.js / image -> pages (nothing is sent anywhere) // readPages(pages, { concurrency, signal, onPage, onEvent, hint, force }) -> { read, failed, stopped, aborted, skippedDone } // plainError(code, status, serverMessage) -> plain-English sentence // session the packet in memory (files, pages, results, decisions, imported steps) // 2. OldReportsScreen the "Old reports" screen of the Upload & Import tool (drop zone, page thumbnails, cost estimate, // progress, "Found in this packet" cards with tie-out checks, import order guide, reference pages, problems). // It never commits anything: "Review & import" hands a dataset to the EXISTING import wizard (UploadTool.jsx.txt). // // Pages are rendered here in the browser (pdf.js from the vendored loader, JPEG <= 2000 px / <= 1.2 MB) and sent one at a time to // POST /api/ai-import/read-page. The Anthropic key lives only on the server. Results are kept in this browser tab's memory only // (they contain customer names): a reload forgets them, so download the CSVs if you want to keep a copy. // All top-level names in this file start with "or" / "Or" (every *.jsx.txt shares one global scope). (function () { "use strict"; const CFG = { maxSide: 2000, // longest side of the JPEG sent to the reader (px) maxBytes: 1200000, // target size of one page (bytes); the server limit is 5 MiB thumbW: 170, // thumbnail width (px) concurrency: 3, // pages read at the same time retries: 2, // automatic retries of a page after 429 / 5xx / network trouble patientRetries: 8, // ... when the 429 says how long to wait (the server's per-minute limit) backoffMs: [2000, 5000], // wait before retry 1 and 2 (the server's Retry-After wins when it is longer) maxWaitMs: 30000, // never wait longer than this between retries timeoutMs: 420000, // one page may take minutes on a dense scan blank: { ink: 0.004, contrast: 40, measureW: 640, margin: 0.05 }, // page is blank when fewer than 0.4 % of the pixels inside its margins are clearly darker than the paper maxFileBytes: 200 * 1024 * 1024, }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const uid = (() => { let n = 0; return (p) => (p || "id") + (++n) + "-" + Math.random().toString(36).slice(2, 6); })(); const mk = (w, h) => { const c = document.createElement("canvas"); c.width = Math.max(1, Math.round(w)); c.height = Math.max(1, Math.round(h)); return c; }; const toBlob = (canvas, q) => new Promise((res, rej) => canvas.toBlob((b) => (b ? res(b) : rej(new Error("The browser could not make a picture of this page."))), "image/jpeg", q)); const toB64 = (blob) => new Promise((res, rej) => { const r = new FileReader(); r.onload = () => { const s = String(r.result); res(s.slice(s.indexOf(",") + 1)); }; r.onerror = () => rej(r.error); r.readAsDataURL(blob); }); const release = (c) => { try { c.width = 0; c.height = 0; } catch (e) {} }; // Fraction of "ink" pixels in the middle of the page: pixels clearly darker than the paper (paper = the 85th percentile brightness). // The outer 5 % is ignored (scanners leave black edge bars and punch-hole shadows there). const inkOf = (src) => { const W = CFG.blank.measureW, H = Math.max(1, Math.round(src.height * W / src.width)); const c = mk(W, H), g = c.getContext("2d", { willReadFrequently: true }); g.fillStyle = "#fff"; g.fillRect(0, 0, W, H); g.drawImage(src, 0, 0, W, H); const d = g.getImageData(0, 0, W, H).data, hist = new Uint32Array(256); const x0 = Math.floor(W * CFG.blank.margin), x1 = W - x0, y0 = Math.floor(H * CFG.blank.margin), y1 = H - y0; const n = Math.max(1, (x1 - x0) * (y1 - y0)), lum = new Uint8Array(n); let i = 0; for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++, i++) { const j = (y * W + x) * 4; const v = (d[j] * 299 + d[j + 1] * 587 + d[j + 2] * 114) / 1000 | 0; lum[i] = v; hist[v]++; } let acc = 0, paper = 255; const target = n * 0.85; for (let v = 0; v < 256; v++) { acc += hist[v]; if (acc >= target) { paper = v; break; } } const cut = paper - CFG.blank.contrast; let ink = 0; for (let k = 0; k < n; k++) if (lum[k] < cut) ink++; release(c); return { ink: ink / n, paper }; }; const rotated = (src, deg) => { deg = ((deg % 360) + 360) % 360; if (!deg) return src; const swap = deg === 90 || deg === 270; const c = mk(swap ? src.height : src.width, swap ? src.width : src.height), g = c.getContext("2d"); g.fillStyle = "#fff"; g.fillRect(0, 0, c.width, c.height); g.translate(c.width / 2, c.height / 2); g.rotate(deg * Math.PI / 180); g.drawImage(src, -src.width / 2, -src.height / 2); return c; }; // canvas -> JPEG that fits maxBytes (quality steps first, then a smaller picture) const fitJpeg = async (canvas, maxBytes) => { let c = canvas, q = 0.86, blob = await toBlob(c, q), guard = 0; while (blob.size > maxBytes && guard++ < 8) { if (q > 0.56) q -= 0.1; else { const n = mk(c.width * 0.85, c.height * 0.85); const g = n.getContext("2d"); g.fillStyle = "#fff"; g.fillRect(0, 0, n.width, n.height); g.drawImage(c, 0, 0, n.width, n.height); if (c !== canvas) release(c); c = n; } blob = await toBlob(c, q); } const out = { blob, width: c.width, height: c.height, bytes: blob.size }; if (c !== canvas) release(c); return out; }; const pdfErr = (e, name) => { const n = (e && e.name) || "", m = String((e && e.message) || e || ""); if (n === "PasswordException" || /password/i.test(m)) return new Error(name + " is password-protected. Open it, save an unprotected copy (or print to PDF) and add that."); if (n === "InvalidPDFException" || /Invalid PDF|not a PDF|Missing PDF/i.test(m)) return new Error(name + " does not look like a valid PDF. Scan or save it again and add the new file."); return new Error("Could not open " + name + ": " + m); }; /* ───────────────────────── reading a file into pages ───────────────────────── */ const isPdf = (f) => /\.pdf$/i.test(f.name || "") || f.type === "application/pdf"; const isImg = (f) => /\.(png|jpe?g)$/i.test(f.name || "") || /^image\/(png|jpeg)$/.test(f.type || ""); async function readFile(file, opts) { opts = opts || {}; const prog = (done, total, label) => { try { opts.onProgress && opts.onProgress({ done, total, label }); } catch (e) {} }; const rot0 = (i) => { const r = typeof opts.rotate === "function" ? opts.rotate(i) : opts.rotate; return ((Math.round((+r || 0) / 90) * 90) % 360 + 360) % 360; }; if (!file) throw new Error("No file."); if (file.size > CFG.maxFileBytes) throw new Error(file.name + " is larger than " + Math.round(CFG.maxFileBytes / 1048576) + " MB. Split it into smaller files."); const fid = uid("f"); if (isPdf(file)) { if (!window.JBImporters || !window.JBImporters.loadPdfJs) throw new Error("The PDF reader is not available in this window (Importers.jsx.txt did not load). Reload the page and try again."); let pdfjs; try { pdfjs = await window.JBImporters.loadPdfJs(); } catch (e) { throw new Error("The PDF reader could not be loaded: " + ((e && e.message) || e) + " Check that /ui_kits/k2b-dms/vendor/ is served, then reload the page."); } let doc; try { doc = await pdfjs.getDocument({ data: new Uint8Array(await file.arrayBuffer()) }).promise; } catch (e) { throw pdfErr(e, file.name); } const total = doc.numPages, pages = []; for (let i = 1; i <= total; i++) { prog(i - 1, total, "Opening " + file.name + " — page " + i + " of " + total); let pg; try { pg = await doc.getPage(i); } catch (e) { throw pdfErr(e, file.name); } const v1 = pg.getViewport({ scale: 1 }); const draw = async (maxSide) => { const sc = maxSide / Math.max(v1.width, v1.height), vp = pg.getViewport({ scale: sc }); const c = mk(vp.width, vp.height), g = c.getContext("2d"); g.fillStyle = "#fff"; g.fillRect(0, 0, c.width, c.height); await pg.render({ canvasContext: g, viewport: vp }).promise; return c; }; const m = await draw(CFG.blank.measureW * 1.5); // small render: thumbnail + blank test const ink = inkOf(m); const th = mk(CFG.thumbW, m.height * CFG.thumbW / m.width), tg = th.getContext("2d"); tg.fillStyle = "#fff"; tg.fillRect(0, 0, th.width, th.height); tg.drawImage(m, 0, 0, th.width, th.height); const thumb = th.toDataURL("image/jpeg", 0.72); release(th); release(m); pages.push({ id: fid + ":p" + i, fileId: fid, fileName: file.name, pageNo: i, totalPages: total, kind: "pdf", w: Math.round(v1.width), h: Math.round(v1.height), thumb, ink: ink.ink, blank: ink.ink < CFG.blank.ink, rotate: rot0(i - 1), excluded: false, forceSend: false, status: "waiting", error: "", code: "", result: null, reads: 0, render: async (rot, shrink) => { const base = await draw(CFG.maxSide * (shrink ? Math.pow(0.7, shrink) : 1)); const r = rotated(base, rot); const j = await fitJpeg(r, CFG.maxBytes * (shrink ? Math.pow(0.6, shrink) : 1)); if (r !== base) release(r); release(base); j.base64 = await toB64(j.blob); j.mediaType = "image/jpeg"; delete j.blob; return j; } }); try { pg.cleanup && pg.cleanup(); } catch (e) {} } prog(total, total, "Opened " + file.name); return { id: fid, name: file.name, size: file.size, kind: "pdf", pages, destroy: () => { try { doc.destroy(); } catch (e) {} } }; } if (isImg(file)) { prog(0, 1, "Opening " + file.name); let bmp; try { bmp = await createImageBitmap(file, { imageOrientation: "from-image" }); } catch (e) { throw new Error("Could not open " + file.name + " as a picture. Use a PNG or JPG file (HEIC photos: share or export them as JPG first)."); } const draw = (maxSide) => { const sc = Math.min(1, maxSide / Math.max(bmp.width, bmp.height)); const c = mk(bmp.width * sc, bmp.height * sc), g = c.getContext("2d"); g.fillStyle = "#fff"; g.fillRect(0, 0, c.width, c.height); g.drawImage(bmp, 0, 0, c.width, c.height); return c; }; const m = draw(CFG.blank.measureW * 1.5), ink = inkOf(m); const th = mk(CFG.thumbW, m.height * CFG.thumbW / m.width), tg = th.getContext("2d"); tg.fillStyle = "#fff"; tg.fillRect(0, 0, th.width, th.height); tg.drawImage(m, 0, 0, th.width, th.height); const thumb = th.toDataURL("image/jpeg", 0.72); release(th); release(m); const page = { id: fid + ":p1", fileId: fid, fileName: file.name, pageNo: 1, totalPages: 1, kind: "image", w: bmp.width, h: bmp.height, thumb, ink: ink.ink, blank: ink.ink < CFG.blank.ink, rotate: rot0(0), excluded: false, forceSend: false, status: "waiting", error: "", code: "", result: null, reads: 0, render: async (rot, shrink) => { const base = draw(CFG.maxSide * (shrink ? Math.pow(0.7, shrink) : 1)); const r = rotated(base, rot); const j = await fitJpeg(r, CFG.maxBytes * (shrink ? Math.pow(0.6, shrink) : 1)); if (r !== base) release(r); release(base); j.base64 = await toB64(j.blob); j.mediaType = "image/jpeg"; delete j.blob; return j; } }; prog(1, 1, "Opened " + file.name); return { id: fid, name: file.name, size: file.size, kind: "image", pages: [page], destroy: () => { try { bmp.close && bmp.close(); } catch (e) {} } }; } throw new Error(file.name + " is not a PDF, PNG or JPG file. (Spreadsheets and CSV files go through the normal Import steps.)"); } /* ───────────────────────── talking to the server ───────────────────────── */ // fatal = stop the whole run (the cause is not this page); auto = retry the page by itself; page = this page failed, retry it alone const CODES = { not_configured: { fatal: true, title: "AI reading is not set up", next: "An administrator needs to open AI Import settings, paste the Anthropic key and press Test connection." }, no_consent: { fatal: true, title: "AI reading has no consent yet", next: "An administrator must tick the consent box in AI Import settings." }, forbidden: { fatal: true, title: "Your role cannot use AI reading", next: "Ask an Administrator, Office Manager or Accounting user to read these pages." }, auth_failed: { fatal: true, title: "Anthropic rejected the key", next: "An administrator should copy the key again from console.anthropic.com > API keys into AI Import settings and press Test connection." }, no_credit: { fatal: true, title: "The Anthropic account has no credit left", next: "Add credit at console.anthropic.com > Plans & Billing, then press Read again - pages already read are kept." }, model_unavailable: { fatal: true, title: "This key cannot use the chosen model", next: "An administrator should pick another model in AI Import settings and press Test connection." }, daily_limit: { fatal: true, title: "The daily page limit is reached", next: "It resets at midnight, or an administrator can raise the limit in AI Import settings." }, session: { fatal: true, title: "Your sign-in expired", next: "Sign in again - pages already read stay in this window." }, rate_limited: { auto: true, title: "Sending pages too fast", next: "K2B waits and continues by itself." }, upstream: { auto: true, title: "The reading service is busy or unreachable", next: "K2B retries by itself; if it keeps failing, retry the page in a few minutes." }, unreadable: { auto: true, title: "The answer could not be understood", next: "Retry the page; if it keeps failing, scan it again more clearly (straight, dark, full page)." }, refused: { page: true, title: "Claude declined to read this page", next: "Retry the page once; if it keeps happening, enter that page by hand or ask an administrator to pick a different model." }, truncated: { page: true, title: "The page is too dense to read in one go", next: "Scan or photograph it in two halves (top and bottom) and add those, or retry once." }, too_large: { page: true, title: "The page picture is too large", next: "K2B shrinks it and tries again by itself." }, bad_request: { page: true, title: "The server could not accept this page", next: "Retry the page." }, network: { auto: true, title: "Could not reach the server", next: "Check the network connection; K2B retries by itself." }, timeout: { auto: true, title: "The server took too long", next: "K2B retries by itself." }, }; const goodSentence = (s) => typeof s === "string" && s.length >= 20 && !/[<>]/.test(s) && /\s/.test(s); function plainError(code, status, msg) { if (goodSentence(msg)) return msg; const c = CODES[code]; if (c) return c.title + ". " + c.next; if (status === 413) return "The page picture is too large for the server. K2B shrinks it and tries again."; if (status === 502 || status === 503 || status === 504) return "The server is busy or restarting. Wait a minute and retry the page."; if (status === 401) return CODES.session.title + ". " + CODES.session.next; if (status === 403) return CODES.forbidden.title + ". " + CODES.forbidden.next; return "Something unexpected happened while reading the page (" + (status || "no answer") + "). Retry the page; if it keeps happening, tell your administrator."; } // POST helper with an abort signal; same headers as apiFetch (session token + station id), never throws for HTTP errors async function post(path, body, signal) { const base = typeof API_BASE !== "undefined" ? API_BASE : location.origin + "/api"; const tok = typeof AUTH_TOKEN !== "undefined" ? AUTH_TOKEN : null; const headers = { "Content-Type": "application/json" }; if (tok) headers["Authorization"] = "Bearer " + tok; try { if (typeof __sessionId !== "undefined" && __sessionId) headers["X-Session-Id"] = __sessionId; } catch (e) {} try { if (tok && window.K2BStore) window.K2BStore.stamp(headers); } catch (e) {} // RC35 (brd): the page's store + protocol (SPEC 4.1) const res = await fetch(base + path, { method: "POST", headers, body: JSON.stringify(body), credentials: "omit", signal }); let text = "", json = null; try { text = await res.text(); json = JSON.parse(text); } catch (e) {} if (res.status === 401 && tok) { try { if (typeof setAuthToken === "function") setAuthToken(null); if (typeof setConnState === "function") setConnState("auth-expired"); } catch (e) {} } return { status: res.status, ok: res.ok, json, retryAfter: parseFloat(res.headers.get("Retry-After")) || 0 }; } // The server limits how many pages one person may send a minute (30 by default). The first 429 teaches the screen how many pages got // through in the last minute; from then on it paces itself under that number, so a long packet does not keep running into the limit. const pace = { limit: 0, stamps: [] }; const paceRecent = () => { const now = Date.now(); pace.stamps = pace.stamps.filter((t) => now - t < 60000); if (pace.limit && now - (pace.learnedAt || 0) > 15 * 60000) pace.limit = 0; return pace.stamps; }; // a limit that was not met for 15 minutes is forgotten async function paceWait(signal) { for (;;) { if (signal && signal.aborted) return; const r = paceRecent(); if (!pace.limit || r.length < pace.limit) return; await sleep(Math.min(CFG.maxWaitMs, Math.max(100, r[0] + 60000 - Date.now() + 50))); } } // Read the pages that are waiting (never a page that is already read, unless opts.force). Resolves when all are done or the run stopped. async function readPages(pages, opts) { const o = Object.assign({ concurrency: CFG.concurrency, signal: null, onPage: null, onEvent: null, hint: "", force: false }, opts || {}); const queue = (pages || []).filter((p) => o.force || p.status !== "done"); const skippedDone = (pages || []).length - queue.length; const ctl = new AbortController(); if (o.signal) { if (o.signal.aborted) ctl.abort(); else o.signal.addEventListener("abort", () => ctl.abort()); } const out = { read: 0, failed: 0, stopped: null, aborted: false, skippedDone }; let next = 0, pauseUntil = 0; const emit = (page, ev) => { try { o.onPage && o.onPage(page, ev); } catch (e) {} }; const note = (ev) => { try { o.onEvent && o.onEvent(ev); } catch (e) {} }; async function one(page) { page.status = "reading"; page.error = ""; page.code = ""; page.retryable = false; page.attempt = 0; emit(page, "start"); let attempt = 0, shrink = 0; for (;;) { if (ctl.signal.aborted) { page.status = "waiting"; emit(page, "aborted"); return; } const wait = pauseUntil - Date.now(); if (wait > 0) { await sleep(Math.min(wait, CFG.maxWaitMs)); continue; } await paceWait(ctl.signal); if (ctl.signal.aborted) continue; let rendered; try { rendered = await page.render(page.rotate, shrink); } catch (e) { page.status = "failed"; page.code = "render"; page.error = "K2B could not turn this page into a picture: " + ((e && e.message) || e) + " Remove the page and add it again."; out.failed++; emit(page, "failed"); return; } const body = { image: { mediaType: rendered.mediaType, data: rendered.base64 }, fileName: page.fileName, pageNo: page.pageNo, totalPages: page.totalPages }; if (o.hint) body.hint = o.hint; page.sentBytes = rendered.bytes; page.sentW = rendered.width; page.sentH = rendered.height; const stamp = Date.now(); pace.stamps.push(stamp); const rc = new AbortController(); const onAbort = () => rc.abort(); ctl.signal.addEventListener("abort", onAbort); const to = setTimeout(() => { page._timedOut = true; rc.abort(); }, CFG.timeoutMs); let r; try { r = await post("/ai-import/read-page", body, rc.signal); } catch (e) { if (ctl.signal.aborted) { clearTimeout(to); page.status = "waiting"; emit(page, "aborted"); return; } r = { status: 0, ok: false, json: null, retryAfter: 0, netErr: page._timedOut ? "timeout" : "network" }; page._timedOut = false; } clearTimeout(to); ctl.signal.removeEventListener("abort", onAbort); const j = r.json || {}; if (r.status === 429 || r.netErr) { const i = pace.stamps.indexOf(stamp); if (i >= 0) pace.stamps.splice(i, 1); } // that request was not accepted // only the K2B server's own per-minute limit teaches the pace (an upstream "Anthropic is slowing this account down" says nothing about it) if (r.status === 429 && j.code === "rate_limited" && /faster than allowed/i.test(j.error || "")) { const got = paceRecent().length; if (got >= 4) { const lim = Math.max(3, got - 1); pace.limit = pace.limit ? Math.min(pace.limit, lim) : lim; pace.learnedAt = Date.now(); } } if (r.ok && j.ok) { page.status = "done"; page.result = j; page.reads++; page.error = ""; out.read++; emit(page, "done"); return; } let code = j.code || r.netErr || (r.status === 401 ? "session" : r.status === 413 ? "too_large" : r.status === 429 ? "rate_limited" : r.status >= 500 || !r.status ? "upstream" : r.status === 403 ? "forbidden" : "bad_request"); const meta = CODES[code] || {}; // the picture was refused as too big / unusable: shrink it and try once or twice more if ((code === "too_large" || (code === "bad_request" && r.status === 422)) && shrink < 2) { shrink++; continue; } // a fatal cause is not this page's fault: stop everything, keep what was read, leave this page waiting if (meta.fatal) { out.stopped = { code, status: r.status, error: plainError(code, r.status, j.error), page: { fileName: page.fileName, pageNo: page.pageNo } }; page.status = "waiting"; page.error = ""; emit(page, "stopped"); note({ type: "stopped", stopped: out.stopped }); ctl.abort(); return; } const retryable = j.retryable !== false && (meta.auto || (r.status === 429 || r.status >= 500 || !r.status)); // a 429 that says how long to wait (the server's own per-minute limit, or Anthropic's Retry-After) is waited out patiently; // anything else is retried twice and then handed to the person const patient = code === "rate_limited" && (+j.retryAfterSec > 0 || r.retryAfter > 0); if (retryable && attempt < (patient ? CFG.patientRetries : CFG.retries) && code !== "daily_limit") { const wait2 = Math.min(CFG.maxWaitMs, Math.max(CFG.backoffMs[Math.min(attempt, CFG.backoffMs.length - 1)], (j.retryAfterSec || r.retryAfter || 0) * 1000)); attempt++; page.attempt = attempt; if (code === "rate_limited") pauseUntil = Math.max(pauseUntil, Date.now() + wait2); note({ type: "retry", code, attempt, waitMs: wait2, page: { fileName: page.fileName, pageNo: page.pageNo } }); emit(page, "retry"); await sleep(wait2); continue; } page.status = "failed"; page.code = code; page.retryable = j.retryable !== false; page.error = plainError(code, r.status, j.error); out.failed++; emit(page, "failed"); return; } } const worker = async () => { while (!ctl.signal.aborted) { const p = queue[next++]; if (!p) return; await one(p); } }; await Promise.all(Array.from({ length: Math.max(1, Math.min(o.concurrency, queue.length)) }, worker)); out.aborted = ctl.signal.aborted && !out.stopped; queue.forEach((p) => { if (p.status === "reading") p.status = "waiting"; }); return out; } window.OldReports = { version: "1.0", config: CFG, readFile, readPages, plainError, CODES, isPdf, isImg, inkOf, pace, _post: post }; })(); /* ───────────────────────── the packet in memory (survives switching between Old reports / Import / History) ───────────────────────── */ const orSession = (() => { const S = { files: [], decisions: {}, imported: {}, adding: null, addErrors: [], run: null, notice: null, rev: 0, rrev: 0 }; // rev = anything changed (re-render); rrev = the read results / decisions changed (rebuild the datasets) const subs = new Set(); let packetCache = null, packetRev = -1; const emit = () => { S.rev++; subs.forEach((f) => { try { f(S); } catch (e) {} }); }; const OR = () => window.OldReports; const allPages = () => S.files.reduce((a, f) => a.concat(f.pages), []); const pageById = (id) => allPages().find((p) => p.id === id); const usable = (p) => !p.excluded && (!p.blank || p.forceSend); // pages the Read button would send now const toRead = () => allPages().filter((p) => usable(p) && p.status === "waiting"); const failedPages = () => allPages().filter((p) => p.status === "failed"); const resultsList = () => { const out = []; allPages().forEach((p) => { if (p.status === "done" && p.result) out.push(Object.assign({}, p.result, { fileName: p.fileName, pageNo: p.pageNo, ok: true })); else if (p.status === "failed") out.push({ fileName: p.fileName, pageNo: p.pageNo, ok: false, error: p.error, code: p.code, retryable: p.retryable !== false }); }); return out; }; // the converter output for everything read so far (recomputed when a page result or a fix changes) let packetAt = 0; const packet = () => { if (packetCache && packetRev === S.rrev) return packetCache; // while pages are coming in, the checks are recomputed at most every 2.5 s (the build is the heavy part; the end of the run always rebuilds) if (packetCache && S.run && S.run.active && Date.now() - packetAt < 2500) return packetCache; const O = window.OldReportsData; let pk = null; try { pk = O && O.buildDatasets ? O.buildDatasets(resultsList(), { decisions: S.decisions }) : null; } catch (e) { pk = null; console.error("OldReportsData.buildDatasets failed", e); } packetCache = pk; packetRev = S.rrev; packetAt = Date.now(); return pk; }; const cost = () => { let usd = 0, inTok = 0, outTok = 0; allPages().forEach((p) => { if (p.result && p.result.usage) { usd += +p.result.usage.costUsd || 0; inTok += +p.result.usage.inputTokens || 0; outTok += +p.result.usage.outputTokens || 0; } }); return { usd, inTok, outTok }; }; const api = { get: () => S, packet, cost, allPages, pageById, toRead, failedPages, usable, resultsList, subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); }, // add files (the same file twice is ignored); each file is opened page by page — nothing is sent anywhere yet async addFiles(list) { const files = Array.from(list || []); if (!files.length) return; S.addErrors = []; for (const f of files) { if (S.files.some((x) => x.name === f.name && x.size === f.size)) { S.addErrors.push({ name: f.name, text: f.name + " is already in this packet." }); emit(); continue; } S.adding = { name: f.name, done: 0, total: 0, label: "Opening " + f.name + "…" }; emit(); try { const rf = await OR().readFile(f, { onProgress: (p) => { S.adding = { name: f.name, done: p.done, total: p.total, label: p.label }; emit(); } }); rf.lastModified = f.lastModified; S.files.push(rf); } catch (e) { S.addErrors.push({ name: f.name, text: (e && e.message) || String(e) }); } } S.adding = null; emit(); }, removeFile(id) { const f = S.files.find((x) => x.id === id); if (!f) return; try { f.destroy && f.destroy(); } catch (e) {} S.files = S.files.filter((x) => x.id !== id); S.rrev++; emit(); }, rotate(pageId, delta) { const p = pageById(pageId); if (!p) return; p.rotate = (((p.rotate + delta) % 360) + 360) % 360; emit(); }, rotateFile(fileId, delta) { const f = S.files.find((x) => x.id === fileId); if (!f) return; f.pages.forEach((p) => { p.rotate = (((p.rotate + delta) % 360) + 360) % 360; }); emit(); }, rotateAll(delta) { allPages().forEach((p) => { p.rotate = (((p.rotate + delta) % 360) + 360) % 360; }); emit(); }, toggleExclude(pageId) { const p = pageById(pageId); if (!p) return; p.excluded = !p.excluded; emit(); }, sendAnyway(pageId) { const p = pageById(pageId); if (!p) return; p.forceSend = !p.forceSend; emit(); }, // read the waiting pages (up to maxPerRun); never a page that is already read async startRun(opts) { opts = opts || {}; if (S.run && S.run.active) return null; let pages = opts.pages ? opts.pages.filter((p) => usable(p)) : toRead(); const maxPerRun = opts.maxPerRun || 100; const queued = pages.length; if (pages.length > maxPerRun) pages = pages.slice(0, maxPerRun); if (!pages.length) return null; const ctl = new AbortController(); S.run = { active: true, total: pages.length, queued, started: Date.now(), ctl, reading: 0, completed: 0, retrying: null, stopped: null }; S.notice = null; emit(); let res; try { res = await OR().readPages(pages, { signal: ctl.signal, force: !!opts.force, onPage: (p, ev) => { if (ev === "done" || ev === "failed") { S.rrev++; S.run.completed++; S.run.retrying = null; } S.run.reading = allPages().filter((x) => x.status === "reading").length; emit(); }, onEvent: (ev) => { if (ev.type === "retry") { S.run.retrying = ev; emit(); } } }); } catch (e) { res = { read: 0, failed: 0, stopped: null, aborted: false, error: e }; } const done = pages.filter((p) => p.status === "done").length, failed = pages.filter((p) => p.status === "failed").length; S.run = { active: false, total: pages.length, queued, done, failed, finished: Date.now(), started: S.run.started, stopped: res.stopped, aborted: res.aborted, left: queued - pages.length }; S.rrev++; emit(); try { window.K2BAiStatus && window.K2BAiStatus.refresh(); } catch (e) {} // pages used today / the key may have stopped working return res; }, cancelRun() { if (S.run && S.run.active && S.run.ctl) { S.run.cancelling = true; S.run.ctl.abort(); emit(); } }, // retry failed pages alone (explicit request) retry(ids) { const ps = (ids ? ids.map(pageById) : failedPages()).filter(Boolean); ps.forEach((p) => { p.status = "waiting"; p.error = ""; p.code = ""; }); S.rrev++; emit(); return api.startRun({ pages: ps }); }, // read a page that was already read once (the user asked for it) reread(pageId) { const p = pageById(pageId); if (!p) return null; p.status = "waiting"; p.result = null; p.error = ""; p.code = ""; S.rrev++; emit(); return api.startRun({ pages: [p], force: true }); }, // accept a suggested fix / a choice between two readings: decisions are kept so a rebuild keeps them applyFix(dsId, fixId) { const pk = packet(); const ds = pk && pk.datasets.find((d) => d.id === dsId); if (!ds) return { ok: false, error: "That list is no longer there." }; try { const nd = window.OldReportsData.applyFix(ds, fixId); S.decisions[dsId] = nd.decisions || []; S.rrev++; emit(); return { ok: true }; } catch (e) { return { ok: false, error: (e && e.message) || String(e) }; } }, // after an import the checks are recomputed: the chart (and so "accounts not in the chart") is different now markImported(dsId, info) { S.imported[dsId] = Object.assign({ at: new Date().toISOString() }, info || {}); S.rrev++; emit(); }, reset() { S.files.forEach((f) => { try { f.destroy && f.destroy(); } catch (e) {} }); S.files = []; S.decisions = {}; S.imported = {}; S.adding = null; S.addErrors = []; S.run = null; S.notice = null; S.rrev++; emit(); }, }; return api; })(); // A reload forgets the pages that were read (and reading them again costs money): ask before the tab is closed while there is something to lose. try { window.addEventListener("beforeunload", (e) => { const S = orSession.get(); const read = orSession.allPages().some((p) => p.status === "done"); const unfinished = read && (!Object.keys(S.imported).length || (orSession.packet() && orSession.packet().datasets.some((d) => !S.imported[d.id]))); if (unfinished || (S.run && S.run.active)) { e.preventDefault(); e.returnValue = ""; return ""; } }); } catch (e) {} const useOrSession = () => { const [, tick] = React.useReducer((n) => n + 1, 0); React.useEffect(() => orSession.subscribe(tick), []); return orSession; }; Object.assign(window.OldReports, { session: orSession }); /* ───────────────────────── the screen ───────────────────────── */ const orBox = { background: "#FFF", border: "1px solid #C9D6E3", borderRadius: 4 }; const orLvl = { ok: { icon: "✓", fg: "#176A38", bg: "#D9F0DE", bd: "#9ED9B1", word: "passed" }, warn: { icon: "⚠", fg: "#8A6400", bg: "#FFF0CC", bd: "#E7D49A", word: "to look at" }, error: { icon: "✕", fg: "#A02234", bg: "#F3D9D9", bd: "#E8B4B4", word: "to fix" }, }; const orPill = (lvl, text, extra) => { const c = orLvl[lvl] || orLvl.ok; return {text}; }; const orMoney = (n) => "$" + (+n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const orTag = (name) => { const b = String(name || "").replace(/\.[^.]+$/, "").replace(/^old-/, ""); const f = b.split("-")[0]; return f && f.length <= 3 ? f : b.slice(0, 16); }; const orRef = (r) => (r && r.fileName ? orTag(r.fileName) + " p." + r.pageNo : ""); const orDownload = (name, text) => { const blob = new Blob([text], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); }; const orTypeLabel = (t) => { const P = window.OldReportsData && window.OldReportsData.PAGE_TYPES; return (P && P[t] && P[t].label) || t || ""; }; const orFileLabel = (ds) => { const names = Array.from(new Set((ds.pages || []).map((p) => p.fileName))); return names.length === 1 ? names[0] : "Old reports packet (" + names.length + " files)"; }; // "Rows to look at": names of the rows (account / stock / deal numbers) a check points at const OrRows = ({ c }) => { const keys = (c.keys && c.keys.length ? c.keys : (c.rows || []).map((r) => "row " + (r + 1))); if (!keys.length) return null; const shown = keys.slice(0, 14); return (
Rows: {shown.map((k, i) => {k})} {keys.length > shown.length && +{keys.length - shown.length} more}
); }; const OrCheck = ({ c }) => { const L = orLvl[c.level] || orLvl.ok; return (
{L.icon}
{c.title}
{c.detail ?
{c.detail}
: null} {c.pages && c.pages.length > 0 &&
Pages: {c.pages.slice(0, 8).map(orRef).join(", ")}{c.pages.length > 8 ? " …" : ""}
}
); }; // two readings of one value that disagree: the person decides const OrChoice = ({ ds, ch }) => { const pick = (opt) => { const r = orSession.applyFix(ds.id, opt.fixId); if (!r.ok && window.k2bToast) window.k2bToast(r.error, "warn", 5000); }; return (
{ch.label}{ch.resolved ? — {ch.by === "user" ? "you chose" : "the majority of the readings"} : — choose the right one}
{(ch.options || []).map((o) => ( ))}
); }; // one "Found in this packet" card const OrCard = ({ ds, no, imp, onReview, onFix }) => { const [openAll, setOpenAll] = React.useState(false); const [confirmRed, setConfirmRed] = React.useState(false); const O = window.OldReportsData; const bad = ds.checks.filter((c) => c.level !== "ok"); const good = ds.checks.filter((c) => c.level === "ok"); const pend = (ds.choices || []).filter((c) => !c.resolved); const settled = (ds.choices || []).filter((c) => c.resolved && c.by === "user"); const st = ds.status || "ok"; const nPages = (ds.pages || []).length; const fileNames = Array.from(new Set((ds.pages || []).map((p) => p.fileName))); const done = imp && !imp.undone; const blocked = pend.length > 0 || ds.importable === false; const label = done ? "Imported ✓ — open again" : st === "error" ? "Review anyway…" : "Review & import ›"; const click = () => { if (blocked) return; if (st === "error" && !done) setConfirmRed(true); else onReview(ds); }; return (
{no}
{ds.sheetName}
Goes to {ds.destLabel} · {ds.rowCount} row{ds.rowCount === 1 ? "" : "s"} · {nPages} page{nPages === 1 ? "" : "s"} ({fileNames.map((n) => n.replace(/\.[^.]+$/, "")).join(", ")})
{done && orPill("ok", "✓ imported " + (imp.counts ? "(" + imp.counts.added + " added, " + imp.counts.updated + " updated)" : ""))} {imp && imp.undone && orPill("warn", "import was undone")} {orPill(st, st === "ok" ? "All checks pass" : st === "warn" ? "Look at " + ds.counts.warn + " thing" + (ds.counts.warn === 1 ? "" : "s") : ds.counts.error + " problem" + (ds.counts.error === 1 ? "" : "s") + " to fix")}
{orPill("ok", "✓ " + ds.counts.ok + " passed")} {ds.counts.warn > 0 && orPill("warn", "⚠ " + ds.counts.warn + " amber")} {ds.counts.error > 0 && orPill("error", "✕ " + ds.counts.error + " red")}
{ds.notes && ds.notes.length > 0 &&
{ds.notes.map((n, i) =>
ℹ {n}
)}
} {pend.length > 0 && (
{pend.length} value{pend.length === 1 ? " was" : "s were"} read two different ways. Look at the printout and choose the right one — nothing is guessed.
{pend.slice(0, 12).map((ch) => )} {pend.length > 12 &&
… and {pend.length - 12} more (choose these first).
}
)} {bad.length > 0 &&
{bad.map((c) => )}
} {ds.fixes && ds.fixes.filter((f) => f.kind !== "choose").length > 0 && (
Suggested fixes: {ds.fixes.filter((f) => f.kind !== "choose").map((f) => ( ))}
)} {settled.length > 0 &&
You chose the reading for {settled.length} value{settled.length === 1 ? "" : "s"}.
} {good.length > 0 && (
{openAll &&
{good.map((c) => )}
}
)}
Nothing is imported from this screen — the next window shows exactly what would be added or changed. {pend.length > 0 && Choose between the readings first.} {ds.importable === false && ds.whyNot && {ds.whyNot}}
{confirmRed && ( setConfirmRed(false)} buttons={<> setConfirmRed(false)}>Cancel }>
{ds.sheetName} has {ds.counts.error} red check{ds.counts.error === 1 ? "" : "s"}: what was read from the pages does not agree with what the pages themselves add up to.
    {bad.filter((c) => c.level === "error").map((c) =>
  • {c.title}
  • )}
You can still open the preview (nothing is saved there either). Check the rows named above against the paper first.
)}
); }; // the ordered "import in this order" guide const OrGuide = ({ pk, S, onReview, impOf }) => { const O = window.OldReportsData; const steps = (O.IMPORT_ORDER || []).map((s, i) => { const dss = pk.datasets.filter((d) => d.dest === s.dest); const done = dss.length > 0 && dss.every((d) => { const m = impOf(d); return m && !m.undone; }); const worst = dss.some((d) => d.needsChoice > 0 || d.importable === false) ? "fix" : dss.some((d) => d.status === "error") ? "red" : dss.some((d) => d.status === "warn") ? "amber" : "ok"; const key = !dss.length ? "missing" : done ? "done" : worst === "fix" ? "fix" : worst === "red" ? "red" : worst === "amber" ? "amber" : "ready"; return { n: i + 1, s, dss, key, done }; }); const firstOpen = steps.find((x) => x.dss.length && !x.done); const chip = { missing: ["off", "— not in this packet"], done: ["ok", "✓ imported"], ready: ["ok", "● ready"], amber: ["warn", "⚠ look at it first"], red: ["error", "✕ has problems"], fix: ["error", "✕ choose / fix first"] }; return (
Import in this order
Each step opens the normal import window with a preview. Doing them in this order keeps the later steps from stumbling over accounts or stock numbers that are not there yet.
{steps.map((x) => (
{x.done ? "✓" : x.n}
{x.s.label}{x === firstOpen ? NEXT : null}
{x.s.why}
{x.dss.length > 1 && (
{x.dss.map((d) => { const m = impOf(d); const dn = m && !m.undone; return ; })}
)}
{x.key === "missing" ? {chip[x.key][1]} : orPill(chip[x.key][0], chip[x.key][1])} {x.dss.length === 1 && }
))}
); }; /* ── page tiles ── */ const orStatusWord = { waiting: ["#666", "waiting"], reading: ["#1B66A8", "reading…"], done: ["#176A38", "read ✓"], failed: ["#A02234", "failed"] }; const OrPageTile = ({ p, onPreview, running }) => { const rot = p.rotate || 0, swap = rot === 90 || rot === 270; const r = p.result, pt = r && r.pageType; const P = window.OldReportsData && window.OldReportsData.PAGE_TYPES; const info = pt && P ? P[pt] : null; const skipped = p.excluded || (p.blank && !p.forceSend); const sw = skipped && p.status === "waiting" ? ["#888", p.excluded ? "left out" : "blank — skipped"] : (orStatusWord[p.status] || orStatusWord.waiting); const small = { fontSize: 10.5, padding: "1px 6px", lineHeight: 1.3 }; return (
onPreview(p)} title="Click to see this page larger" style={{ height: 196, display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden", background: "#F2F4F7", border: "1px solid #E3E8EE", cursor: "zoom-in", position: "relative" }}> {"page {p.status === "reading" && ⏳}
p. {p.pageNo} {sw[1]}{p.attempt && p.status === "reading" ? " (try " + (p.attempt + 1) + ")" : ""} {rot ? ↻{rot}° : null}
{r &&
{info ? info.label : orTypeLabel(pt)}{r.legibility && r.legibility !== "good" ? · {r.legibility} print : null}
} {p.status === "failed" &&
{p.error}
}
{p.blank && !p.excluded && p.status === "waiting" && } {p.status === "failed" && } {p.status === "done" && }
); }; // a page, larger (rendered fresh so you can check the turn before reading it) const OrPagePreview = ({ p, onClose }) => { const [src, setSrc] = React.useState(""); const [err, setErr] = React.useState(""); const rot = p.rotate || 0; React.useEffect(() => { let alive = true, url = ""; setSrc(""); setErr(""); p.render(rot, 1).then((j) => { if (!alive) return; url = "data:image/jpeg;base64," + j.base64; setSrc(url); }).catch((e) => alive && setErr((e && e.message) || String(e))); return () => { alive = false; }; }, [p.id, rot]); return ( orSession.rotate(p.id, -90)}>↺ Turn left orSession.rotate(p.id, 90)}>↻ Turn right Close }>
{err ?
{err}
: src ? page :
Drawing the page…
}
); }; /* ── the main screen ── */ const OldReportsScreen = ({ onReview, go, onOpenHistory }) => { const S = useOrSession(); const st = S.get(); const ai = window.useK2BAiStatus ? window.useK2BAiStatus() : { canUse: false, key: "loading", chip: "", headline: "", next: "", tone: "off", admin: false, pricing: null, limits: null }; const D = window.DMS, O = window.OldReportsData; const [, tickD] = React.useReducer((n) => n + 1, 0); React.useEffect(() => (D && D.subscribe ? D.subscribe(tickD) : undefined), []); const [showAi, setShowAi] = React.useState(false); const [preview, setPreview] = React.useState(null); const [drag, setDrag] = React.useState(false); const [confirmReset, setConfirmReset] = React.useState(false); const [showCross, setShowCross] = React.useState(false); const [pagesOpen, setPagesOpen] = React.useState(null); // null = automatic: open while there is nothing to review yet or a run is going const fileRef = React.useRef(null); const pk = S.packet(); const pages = S.allPages(); const run = st.run; const running = !!(run && run.active); const toRead = S.toRead(); const failed = S.failedPages(); const nBlank = pages.filter((p) => p.blank && !p.excluded && !p.forceSend && p.status === "waiting").length; const nOut = pages.filter((p) => p.excluded).length; const nDone = pages.filter((p) => p.status === "done").length; const maxRun = (ai.limits && ai.limits.maxPagesPerRun) || 100; const batch = Math.min(toRead.length, maxRun); const est = O && O.estimateCost ? O.estimateCost(toRead.length, ai.pricing) : null; const cost = S.cost(); const left = ai.limits ? Math.max(0, ai.limits.maxPagesPerDay - ai.limits.pagesToday) : null; const impOf = (ds) => { const m = st.imported[ds.id]; if (!m) return null; const h = D && D.importHistory ? (D.importHistory() || []).find((x) => x.id === m.historyId) : null; return h && h.undone ? Object.assign({}, m, { undone: true }) : m; }; const doReview = (ds) => onReview && onReview(ds, { fileName: orFileLabel(ds) }); const onFix = (ds, f) => { const r = orSession.applyFix(ds.id, f.id); window.k2bToast && window.k2bToast(r.ok ? "Done: " + f.label + ". Check the list again below." : r.error, r.ok ? "ok" : "warn", 4200); }; const addFiles = (list) => { if (list && list.length) orSession.addFiles(list); }; const startRead = () => { if (!ai.canUse || running || !toRead.length) return; orSession.startRun({ maxPerRun: maxRun }); }; const adding = st.adding; const reviewable = !!(pk && (pk.datasets.length || pk.reference.length || pk.problems.length || pk.blankPages.length)); const pagesShown = pagesOpen !== null ? pagesOpen : (!reviewable || running || failed.length > 0 || toRead.length > 0); // folded away only when everything was read without trouble const secs = run && run.finished && run.started ? Math.round((run.finished - run.started) / 1000) : 0; return (
📄 Old reports — read a packet of printouts
PDFs or photos of the old DMS reports → lists you can check, then import with the normal preview
{window.K2BAiChip && } {pages.length > 0 && }
{/* AI reading status banner (the same status as AI Import settings and Settings > Integrations) */} {!ai.loading && !ai.canUse && (
{window.K2BAiChip ? : null}{ai.headline}
{ai.next &&
Next: {ai.next}
}
You can still add files and check the pages below; reading needs AI reading to be on.
{ai.admin && ai.key !== "role" && }
)} {/* drop zone */}
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); addFiles(e.dataTransfer && e.dataTransfer.files); }} style={{ ...orBox, border: "2px dashed " + (drag ? "#0E7A37" : "#1B66A8"), background: drag ? "#EAF7EE" : "#F1F7FC", padding: pages.length ? "10px 14px" : "22px 16px", display: "flex", flexDirection: "column", gap: 8, alignItems: pages.length ? "stretch" : "center", textAlign: pages.length ? "left" : "center" }}> { addFiles(e.target.files); if (fileRef.current) fileRef.current.value = ""; }} />
{pages.length ? "Add more PDFs or photos" : "Drop the scanned PDFs or photos of the old reports here"}
{!pages.length &&
PDF, PNG or JPG — as many files as you have. Trial balance, chart of accounts, deal recaps, vehicle master, aged schedules and journal documents are recognised; pages that cannot be imported (balance sheet, screens) are kept for reference. The pages are opened here in your browser first — nothing is sent until you press Read. Reading sends each page to Anthropic's API through your K2B server; the key never reaches this browser.
} {adding &&
⏳ {adding.label}{adding.total ? " (" + adding.done + " of " + adding.total + ")" : ""}
} {st.addErrors.map((e, i) =>
⚠ {e.text}
)}
{/* pages + cost + progress */} {pages.length > 0 && (
Pages {pages.length} page{pages.length === 1 ? "" : "s"} in {st.files.length} file{st.files.length === 1 ? "" : "s"} {nBlank > 0 && <> · {nBlank} blank (skipped)} {nOut > 0 && <> · {nOut} left out} {nDone > 0 && <> · {nDone} read} {failed.length > 0 && <> · {failed.length} failed} Turn all pages:
{/* what reading will cost + the Read button */}
{running ? (
Pages are being read. The lists you can check appear here when reading is finished; the pages below show what each one turned out to be.
) : toRead.length > 0 ? ( <>
{toRead.length} page{toRead.length === 1 ? "" : "s"} will be read{toRead.length > maxRun ? " (" + maxRun + " at a time — press Read again for the rest)" : ""}.
{est ? est.text : ""}
{left !== null && toRead.length > left &&
⚠ Today's limit allows only {left} more page{left === 1 ? "" : "s"} (an administrator can raise it in AI Import settings).
}
Blank pages are never sent. Turn a sideways or upside-down page now if you can see it; the reader copes with turned pages, but a straight page is read more reliably.
) : (
{nDone && !failed.length ? "Every page has been read — what was found is listed below." : failed.length ? "Some pages failed — retry them below, or retry just one page." : "Nothing to read: every page is blank or left out."}
)}
{!running && (toRead.length > 0 || !nDone) && } {running && } {failed.length > 0 && !running && }
{!ai.canUse && toRead.length > 0 && !ai.loading &&
Reading is not possible yet: {ai.headline}
} {run && (run.active || run.finished) && (
{run.active ? (
⏳ Reading — {run.completed} of {run.total} done, {run.reading} being read now (up to 3 at a time) · spent so far {orMoney(cost.usd)} {run.retrying &&
The server asked K2B to wait ({run.retrying.code === "rate_limited" ? "too many pages a minute" : "temporary trouble"}); trying again in about {Math.round(run.retrying.waitMs / 1000)} s — nothing is lost.
}
) : (
{run.stopped ?
Reading stopped. {run.stopped.error}
{run.done} page{run.done === 1 ? "" : "s"} read before it stopped are kept below. {toRead.length ? "Fix the cause above, then press Read again — only the " + toRead.length + " unread page" + (toRead.length === 1 ? "" : "s") + " are sent." : ""}
: run.aborted ? ■ Stopped by you after {run.done} page{run.done === 1 ? "" : "s"} — what was read is kept. Press Read to continue with the rest. : ✓ Finished: {run.done} read{run.failed ? ", " + run.failed + " failed (retry them below)" : ""}{run.left ? ", " + run.left + " more waiting (press Read again)" : ""} in {secs} s · spent {orMoney(cost.usd)}.}
)}
)} {nDone > 0 && cost.usd > 0 && !running &&
Spent so far on this packet: {orMoney(cost.usd)} ({cost.inTok.toLocaleString("en-US")} tokens in, {cost.outTok.toLocaleString("en-US")} out). A page that is already read is never sent again unless you press Read again on it.
}
)} {/* results */} {pk && reviewable && !running && (
Found in this packet {pk.datasets.length} list{pk.datasets.length === 1 ? "" : "s"} to import · {pk.overall.rows.toLocaleString("en-US")} rows · {pk.reference.reduce((s, r) => s + r.count, 0)} reference page{pk.reference.reduce((s, r) => s + r.count, 0) === 1 ? "" : "s"} · {pk.blankPages.length + nBlank} blank page{pk.blankPages.length + nBlank === 1 ? "" : "s"} {orPill(pk.overall.status === "error" ? "error" : pk.overall.status === "warn" ? "warn" : "ok", pk.overall.status === "error" ? pk.overall.errors + " red" : pk.overall.status === "warn" ? pk.overall.warnings + " amber" : "all clear")} {onOpenHistory && }
{pk.datasets.length > 0 && }
{pk.datasets.map((ds, i) => )}
{pk.crossChecks.length > 0 && (
Reports in the packet are compared with each other (for example the balance sheet's profit against the trial balance).
{showCross &&
{pk.crossChecks.map((c, i) => )}
}
)} {pk.reference.length > 0 && (
Kept for reference — nothing is imported from these pages
{pk.reference.map((r) => (
{r.label}{r.count} page{r.count === 1 ? "" : "s"}: {r.pages.map(orRef).join(", ")} {r.checks.some((c) => c.level === "error") ? orPill("error", "✕ check") : r.checks.some((c) => c.level === "warn") ? orPill("warn", "⚠ check") : r.checks.length ? orPill("ok", "✓ " + r.checks.length + " check" + (r.checks.length === 1 ? "" : "s") + " pass") : null}
{r.why}
{r.note ?
{r.note}
: null} {r.checks.filter((c) => c.level !== "ok").map((c, i) =>
)}
))}
)} {pk.problems.length > 0 && (
Pages that need your attention
{pk.problems.map((pr, i) => { const pg = pr.page ? pages.find((p) => p.fileName === pr.page.fileName && p.pageNo === pr.page.pageNo) : null; return (
{pr.level === "error" ? "✕" : "⚠"}
{pr.title}{pr.page ? — {orRef(pr.page)} : null}
{pr.detail}
{pg && pg.status === "failed" && } {pg && pg.status === "done" && pr.code === "poor_legibility" && }
); })}
)}
)} {/* the pages themselves: open while reading, folded away once there is something to look at */} {pages.length > 0 && (
{pagesShown ? "Click a page to see it larger. Turn a page with ↺ ↻, or leave it out." : "Thumbnails, turning pages, leaving a page out, reading one page again."}
{pagesShown && (<> {st.files.map((f) => (
{f.name} {f.pages.length} page{f.pages.length === 1 ? "" : "s"} Turn every page of this file:
{f.pages.map((p) => )}
))} )}
)}
{showAi && ( { setShowAi(false); window.K2BAiStatus && window.K2BAiStatus.refresh(); }} buttons={ { setShowAi(false); window.K2BAiStatus && window.K2BAiStatus.refresh(); }}>Close}> {window.AiImportSettingsPanel ? :
Integrations.jsx.txt is not loaded.
}
)} {preview && setPreview(null)} />} {confirmReset && ( setConfirmReset(false)} buttons={<> setConfirmReset(false)}>Keep working }>
This clears the pages and everything that was read from them from this window. Reading them again costs money. Anything you already imported stays imported. Download the CSV files first if you want to keep the lists.
)}
); }; Object.assign(window, { OldReportsScreen });