/* =============================================================
   CoreIQ Office — Invoice capture: upload -> AI parse -> confirm -> post.
   Three screens: Upload (drop zone + settings), Capture queue (files and
   their parse status), Confirm (original image left, extracted fields
   right, CHECKS + Post/Save draft/Reject at the bottom).

   Nothing here posts money on its own — every Post click calls
   OfficeAPI.postInvoiceCapture, which the API answers by calling the SAME
   createManualInvoice the hand-keyed "Enter invoice" modal uses. Auto-post
   (when enabled at upload) is a server-side decision made from facts, never
   from the confidence numbers this screen displays — those are shown here
   purely so a human knows where to look.

   Uses shared bare globals: OIcon, Screen, StatusPill, window.useOffice,
   window.OfficeAPI (see office-screens-expand-1.jsx / office-app.jsx).
   Prefix: icap*
   ============================================================= */
const { useState: icapUseState, useEffect: icapUseEffect, useCallback: icapUseCallback, useRef: icapUseRef } = React;

const icapMoney = (c) => (c == null ? "—" : "$" + (Number(c) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
const icapDate = (s) => (s ? String(s).slice(0, 10) : "—");
const icapMaxMb = 20;
const icapAccept = ".pdf,.png,.jpg,.jpeg,.tif,.tiff,application/pdf,image/png,image/jpeg,image/tiff";
const icapAcceptedTypes = ["application/pdf", "image/png", "image/jpeg", "image/jpg", "image/tiff"];

function icapConfKind(c) {
  if (c == null) return "neutral";
  if (c >= 0.85) return "paid";
  if (c >= 0.6) return "hold";
  return "void";
}
function IcapConfBadge({ confidence, reason }) {
  if (confidence == null) return null;
  const pct = Math.round(confidence * 100);
  return (
    <span className={"badge badge--" + icapConfKind(confidence)} title={reason || pct + "% confidence"}>
      <span className="badge-dot" /> {pct}%
    </span>
  );
}

// ---------------------------------------------------------------------
// Upload
// ---------------------------------------------------------------------
function IcapUploadScreen() {
  const { pushToast, setScreen } = window.useOffice();
  const [sites, setSites] = icapUseState([]);
  const [siteId, setSiteId] = icapUseState("");
  const [autoPost, setAutoPost] = icapUseState(false);
  const [splitMulti, setSplitMulti] = icapUseState(false);
  const [files, setFiles] = icapUseState([]); // [{file, error}]
  const [dragging, setDragging] = icapUseState(false);
  const [busy, setBusy] = icapUseState(false);
  const inputRef = icapUseRef(null);

  icapUseEffect(() => {
    let alive = true;
    window.OfficeAPI.listSites().then((ss) => {
      if (!alive) return;
      setSites(ss || []);
      setSiteId((cur) => cur || ((ss && ss[0]) ? ss[0].id : ""));
    }).catch(() => {});
    return () => { alive = false; };
  }, []);

  function addFiles(list) {
    const next = Array.from(list).map((f) => {
      const okType = icapAcceptedTypes.includes(f.type) || /\.(pdf|png|jpe?g|tiff?)$/i.test(f.name);
      const okSize = f.size <= icapMaxMb * 1024 * 1024;
      const error = !okType ? "Unsupported type — PDF, PNG, JPG or TIFF only" : !okSize ? "Over " + icapMaxMb + "MB" : null;
      return { file: f, error };
    });
    setFiles((fs) => fs.concat(next));
  }
  function removeFile(i) { setFiles((fs) => fs.filter((_, j) => j !== i)); }

  const validFiles = files.filter((f) => !f.error);
  const canParse = !!siteId && validFiles.length > 0 && !busy;

  function parse() {
    if (!canParse) return;
    setBusy(true);
    window.OfficeAPI.uploadInvoiceCaptureFiles(validFiles.map((f) => f.file), { siteId, autoPost, splitMulti })
      .then((r) => {
        setBusy(false);
        const okCount = (r.files || []).length;
        if (okCount > 0) {
          pushToast({ kind: "paid", icon: "check", title: okCount + " file" + (okCount === 1 ? "" : "s") + " uploaded", meta: "Parsing now — track progress in the Capture queue" });
        }
        if (r.errors && r.errors.length) {
          pushToast({ kind: "hold", icon: "alert", title: "Some files were skipped", meta: r.errors.slice(0, 2).join("; ") });
        }
        setScreen("invoiceCaptureQueue");
      })
      .catch((e) => { setBusy(false); pushToast({ kind: "hold", icon: "alert", title: "Upload failed", meta: String((e && e.message) || e) }); });
  }

  return (
    <Screen
      eyebrow="Buying · invoice capture"
      title="Upload invoices"
      actions={<button className="btn btn--ghost btn--sm" onClick={() => setScreen("invoiceCaptureQueue")}><OIcon name="list" /> Capture queue</button>}
    >
      <div style={{ display: "grid", gridTemplateColumns: "1.6fr 1fr", gap: 20, alignItems: "start" }}>
        <div>
          <div
            className={"icap-drop" + (dragging ? " icap-drop--active" : "")}
            onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
            onDragLeave={() => setDragging(false)}
            onDrop={(e) => { e.preventDefault(); setDragging(false); if (e.dataTransfer.files) addFiles(e.dataTransfer.files); }}
            onClick={() => inputRef.current && inputRef.current.click()}
          >
            <OIcon name="doc" width={28} height={28} />
            <div className="icap-drop-title">Drop invoices here, or click to browse</div>
            <div className="icap-drop-sub">PDF, PNG, JPG or TIFF · up to {icapMaxMb}MB each · several at once</div>
            <input
              ref={inputRef} type="file" multiple accept={icapAccept} style={{ display: "none" }}
              onChange={(e) => { if (e.target.files) addFiles(e.target.files); e.target.value = ""; }}
            />
          </div>

          {files.length > 0 && (
            <div className="icap-filelist">
              {files.map((f, i) => (
                <div key={i} className={"icap-filerow" + (f.error ? " icap-filerow--error" : "")}>
                  <OIcon name="doc" width={15} height={15} />
                  <span className="icap-filerow-name">{f.file.name}</span>
                  <span className="icap-filerow-size">{(f.file.size / 1024 / 1024).toFixed(1)}MB</span>
                  {f.error && <span className="icap-filerow-err">{f.error}</span>}
                  <button className="btn btn--ghost btn--sm" onClick={() => removeFile(i)}><OIcon name="x" width={13} height={13} /></button>
                </div>
              ))}
            </div>
          )}

          <div className="field" style={{ marginTop: 16, maxWidth: 320 }}>
            <label className="field-label">Charge to store <span className="field-required">*</span></label>
            <select className="input select" value={siteId} onChange={(e) => setSiteId(e.target.value)}>
              <option value="">Choose…</option>
              {sites.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
            </select>
          </div>

          <div style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 16 }}>
            <label className="toggle">
              <input type="checkbox" checked={autoPost} onChange={(e) => setAutoPost(e.target.checked)} />
              <span className="toggle-track"><span className="toggle-thumb" /></span>
              <span style={{ fontSize: 13 }}>Auto-post clean invoices</span>
            </label>
            <label className="toggle">
              <input type="checkbox" checked={splitMulti} onChange={(e) => setSplitMulti(e.target.checked)} />
              <span className="toggle-track"><span className="toggle-thumb" /></span>
              <span style={{ fontSize: 13 }}>Split multi-invoice files</span>
            </label>
          </div>

          <div style={{ marginTop: 20, display: "flex", justifyContent: "flex-end" }}>
            <button className="btn btn--primary" disabled={!canParse} onClick={parse}>
              {busy ? <span className="spinner" /> : <OIcon name="check" />} Parse
            </button>
          </div>
        </div>

        <div className="icap-explainer">
          <h4>What happens next</h4>
          <ol>
            <li>Each file is read — text-native PDFs are transcribed directly; scans and photos go to a vision model. Nothing is guessed that isn't on the page.</li>
            <li>The parsed invoice lands in the <strong>Capture queue</strong> for you to check.</li>
            <li>Nothing posts until you've reviewed it side-by-side with the original — <strong>unless</strong> "Auto-post clean invoices" is on above, and even then only once the numbers themselves check out (never a confidence score): the supplier is known, the invoice number isn't a duplicate, and the lines add up to the printed total.</li>
            <li>Unmatched line items never create a new product at a guessed cost — they go to the existing Barcode queue after receiving, same as any other invoice.</li>
            <li>A service invoice (no goods — cleaning, software, a professional fee) is captured and shown but never posted; Office has no ledger to code it to.</li>
          </ol>
        </div>
      </div>
    </Screen>
  );
}

// ---------------------------------------------------------------------
// Capture queue — file-first, one file usually has one capture nested
// ---------------------------------------------------------------------
// Where a capture sits in the whole journey, which does NOT end at "posted".
// Posting hands the invoice to Receiving; the stock only exists once someone
// receives it. A capture that has been posted but not yet received is still
// somebody's job, so it must not sit in the same pile as the finished ones.
function icapStage(c) {
  if (c.status === "rejected") return "done";
  if (c.status === "posted") {
    // Service invoices are captured and never posted, so they can't get here.
    return c.postedInvoiceStatus === "received" ? "done" : "receipt";
  }
  if (c.status === "review") return c.autoPostEligible ? "ready" : "attention";
  return "attention"; // parsing/failed captures need a human eventually
}

const ICAP_TABS = [
  { k: "attention", l: "Needs attention" },
  { k: "ready", l: "Ready to confirm" },
  { k: "receipt", l: "Awaiting receipt" },
  { k: "done", l: "Done" },
  { k: "all", l: "All" },
];

function IcapQueueScreen() {
  const { pushToast, setScreen, setScreenState } = window.useOffice();
  const [files, setFiles] = icapUseState(null);
  const [err, setErr] = icapUseState(null);
  const [tab, setTab] = icapUseState("attention");
  const [confirmDelete, setConfirmDelete] = icapUseState(null);

  const load = icapUseCallback(() => {
    window.OfficeAPI.listInvoiceCaptureFiles()
      .then((fs) => { setFiles(fs || []); setErr(null); })
      .catch((e) => setErr(String((e && e.message) || e)));
  }, []);
  icapUseEffect(() => {
    load();
    // Parsing (esp. a multi-page vision call) can take a while — poll rather
    // than leave the queue looking frozen on "Parsing…".
    const t = setInterval(load, 5000);
    return () => clearInterval(t);
  }, [load]);

  function openCapture(id) {
    setScreenState((st) => Object.assign({}, st, { invoiceCaptureId: id }));
    setScreen("invoiceCaptureConfirm");
  }
  function discard(f) {
    if (confirmDelete !== f.id) { setConfirmDelete(f.id); return; }
    setConfirmDelete(null);
    window.OfficeAPI.deleteInvoiceCaptureFile(f.id)
      .then(() => { pushToast({ kind: "paid", icon: "check", title: "Upload discarded", meta: f.originalFilename }); load(); })
      .catch((e) => pushToast({ kind: "hold", icon: "alert", title: "Couldn't discard it", meta: String((e && e.message) || e) }));
  }
  function retry(id) {
    window.OfficeAPI.reparseInvoiceCaptureFile(id)
      .then(() => { pushToast({ kind: "paid", icon: "check", title: "Re-parsing" }); load(); })
      .catch((e) => pushToast({ kind: "hold", icon: "alert", title: "Couldn't retry", meta: String((e && e.message) || e) }));
  }

  const fileStatusPill = (f) => {
    if (f.status === "uploaded" || f.status === "parsing") return <StatusPill kind="neutral" label="Parsing…" />;
    if (f.status === "failed") return <StatusPill kind="void" label="Failed" />;
    return <StatusPill kind="paid" label="Parsed" />;
  };
  const captureStatusPill = (c) => {
    if (c.status === "review") return c.autoPostEligible ? <StatusPill kind="paid" label="Ready to confirm" /> : <StatusPill kind="hold" label="Needs a look" />;
    if (c.status === "posted") {
      // Say which of the two "posted" states this is. "Posted" alone reads as
      // finished, and it isn't until the stock is on the ledger.
      if (c.postedInvoiceStatus === "received") return <StatusPill kind="paid" label="Received" />;
      if (c.postedInvoiceStatus === "review") return <StatusPill kind="hold" label="Posted · needs attention in Receiving" />;
      return <StatusPill kind="brand" label="Posted · awaiting receipt" />;
    }
    if (c.status === "rejected") return <StatusPill kind="void" label="Rejected" />;
    return <StatusPill kind="neutral" label={c.status} />;
  };

  // Filter the CAPTURES inside each file, then drop files left with none —
  // split-multi means one file can hold captures at different stages.
  const parsingCount = (files || []).filter((f) => f.status === "uploaded" || f.status === "parsing").length;
  const counts = { attention: 0, ready: 0, receipt: 0, done: 0, all: 0 };
  for (const f of files || []) {
    for (const c of f.captures || []) { counts[icapStage(c)] += 1; counts.all += 1; }
  }
  const shown = (files || [])
    .map((f) => {
      if (tab === "all") return f;
      const caps = (f.captures || []).filter((c) => icapStage(c) === tab);
      // A file that failed to parse has no captures at all — it belongs in
      // Needs attention, since only a person can decide to retry it.
      if (caps.length === 0 && !(tab === "attention" && (f.status === "failed" || (f.captures || []).length === 0))) return null;
      return Object.assign({}, f, { captures: caps });
    })
    .filter(Boolean);

  return (
    <Screen
      eyebrow="Buying · invoice capture"
      title="Capture queue"
      subtabs={ICAP_TABS.map((t) => ({ k: t.k, l: t.l, badge: t.k === "attention" ? (counts.attention || null) : t.k === "ready" ? (counts.ready || null) : null }))}
      activeTab={tab}
      onTab={setTab}
      actions={
        <span style={{ display: "flex", gap: 8 }}>
          <button className="btn btn--primary btn--sm" onClick={() => setScreen("invoiceCaptureUpload")}><OIcon name="plus" /> Upload</button>
          <button className="btn btn--ghost btn--sm" onClick={load}>Refresh</button>
        </span>
      }
    >
      {/* Parsing is transient and worth seeing whichever tab you are on —
          otherwise a fresh upload looks like it vanished. */}
      {parsingCount > 0 && (
        <div className="off-empty" style={{ marginBottom: 12 }}>
          <span className="spinner" /> Reading {parsingCount} {parsingCount === 1 ? "file" : "files"}…
        </div>
      )}
      {err && <div className="off-empty"><OIcon name="alert" /> Couldn't load: {err}</div>}
      {files === null && !err && <div className="off-empty"><span className="spinner" /> Loading…</div>}
      {files && files.length === 0 && !err && (
        <div className="off-empty">
          <div className="empty-icon"><OIcon name="doc" width={20} height={20} /></div>
          Nothing uploaded yet.
        </div>
      )}
      {files && files.length > 0 && shown.length === 0 && !err && (
        <div className="off-empty">
          {tab === "attention" ? "Nothing needs attention — everything read cleanly."
            : tab === "ready" ? "Nothing waiting to be confirmed."
            : tab === "receipt" ? "Nothing waiting on receipt — every posted invoice has had its stock landed."
            : tab === "done" ? "Nothing finished yet."
            : "Nothing here."}
        </div>
      )}
      {files && shown.length > 0 && (
        <div className="icap-filecards">
          {shown.map((f) => (
            <div key={f.id} className="icap-filecard">
              <div className="icap-filecard-head">
                <OIcon name="doc" />
                <span className="icap-filecard-name" title={f.originalFilename}>{f.originalFilename}</span>
                {fileStatusPill(f)}
                {f.status === "failed" && (
                  <button className="btn btn--ghost btn--sm" onClick={() => retry(f.id)}><OIcon name="refresh" width={13} height={13} /> Retry</button>
                )}
                {/* Not offered once something here has been posted — the API
                    refuses that too, since the document is the record of a
                    real invoice. A shipping note that is not an invoice at all
                    should just go away rather than sit in the queue forever. */}
                {!(f.captures || []).some((c) => c.status === "posted") && (
                  <button
                    className="btn btn--ghost btn--sm"
                    style={confirmDelete === f.id ? { color: "var(--void-text)", fontWeight: 600 } : undefined}
                    onClick={() => discard(f)}
                    onBlur={() => setConfirmDelete((v) => (v === f.id ? null : v))}
                    title="Remove this upload and anything read off it"
                  >
                    <OIcon name="x" width={13} height={13} /> {confirmDelete === f.id ? "Tap again to discard" : "Discard"}
                  </button>
                )}
              </div>
              {f.errorReason && <div className="icap-filecard-error">{f.errorReason}</div>}
              {f.captures.length > 0 && (
                <div className="icap-capturelist">
                  {f.captures.map((c) => (
                    <div key={c.id} className="icap-capturerow" onClick={() => openCapture(c.id)}>
                      <span className="icap-capturerow-supplier">{c.supplierName || "Unknown supplier"}</span>
                      <span className="icap-capturerow-inv">{c.invoiceNumber || "no number read"}</span>
                      <span className="icap-capturerow-kind">{c.kind === "service" ? "Service — not posted" : ""}</span>
                      <span className="icap-capturerow-total">{icapMoney(c.totalCents)}</span>
                      {captureStatusPill(c)}
                      {icapStage(c) === "receipt" && (
                        // The next action isn't on this screen — the stock
                        // lands from the Receiving worklist. Say so, and go.
                        <button
                          className="btn btn--ghost btn--sm"
                          onClick={(e) => { e.stopPropagation(); setScreen("receiving"); }}
                        >
                          Receive it
                        </button>
                      )}
                      <OIcon name="chevron-right" width={14} height={14} />
                    </div>
                  ))}
                </div>
              )}
            </div>
          ))}
        </div>
      )}
    </Screen>
  );
}

// ---------------------------------------------------------------------
// Confirm — image LEFT, fields RIGHT, checks + actions pinned at bottom
// ---------------------------------------------------------------------
function IcapMoneyInput({ cents, onCommit }) {
  const [v, setV] = icapUseState(cents == null ? "" : (cents / 100).toFixed(2));
  icapUseEffect(() => { setV(cents == null ? "" : (cents / 100).toFixed(2)); }, [cents]);
  return (
    <input
      className="input" inputMode="decimal" value={v} placeholder="0.00"
      onChange={(e) => setV(e.target.value)}
      onBlur={() => { const num = Number(v); onCommit(v.trim() === "" ? null : Math.round(num * 100)); }}
    />
  );
}

function IcapField({ label, confidence, reason, children }) {
  const low = confidence != null && confidence < 0.85;
  return (
    <div className="icap-field">
      <div className="icap-field-head">
        <span className="icap-field-label">{label}</span>
        <IcapConfBadge confidence={confidence} reason={reason} />
      </div>
      {children}
      {reason && low && <div className="icap-field-reason"><OIcon name="alert" width={12} height={12} /> {reason}</div>}
    </div>
  );
}

function IcapLineCard({ line, editable, onPatch }) {
  const [desc, setDesc] = icapUseState(line.confirmedDescription ?? line.descriptionRaw);
  const [qty, setQty] = icapUseState(String(line.confirmedQuantity ?? line.quantityRaw ?? ""));
  const [cost, setCost] = icapUseState((((line.confirmedUnitCostCents ?? line.unitCostCentsRaw) || 0) / 100).toFixed(2));
  const [gstFree, setGstFree] = icapUseState(!!(line.confirmedGstFree ?? line.gstFreeRaw));

  icapUseEffect(() => {
    setDesc(line.confirmedDescription ?? line.descriptionRaw);
    setQty(String(line.confirmedQuantity ?? line.quantityRaw ?? ""));
    setCost((((line.confirmedUnitCostCents ?? line.unitCostCentsRaw) || 0) / 100).toFixed(2));
    setGstFree(!!(line.confirmedGstFree ?? line.gstFreeRaw));
  }, [line.id, line.confirmedDescription, line.confirmedQuantity, line.confirmedUnitCostCents, line.confirmedGstFree]);

  const lineTotalCents = (Number(qty) || 0) * Math.round((Number(cost) || 0) * 100);
  const matchedSuggestion = (line.suggestedMatches || []).find((s) => s.productId === line.matchedProductId);

  return (
    <div className={"icap-linecard" + (line.status === "confirmed" ? " icap-linecard--confirmed" : "")}>
      <div className="icap-linecard-main">
        {editable ? (
          <input
            className="input icap-linecard-desc" value={desc} onChange={(e) => setDesc(e.target.value)}
            onBlur={() => { if (desc.trim() && desc !== (line.confirmedDescription ?? line.descriptionRaw)) onPatch({ confirmedDescription: desc }); }}
          />
        ) : <span className="icap-linecard-desc-text">{desc}</span>}
        <span className="icap-linecard-qtycost">
          {editable ? (
            <>
              <input className="input icap-qty" inputMode="numeric" value={qty} onChange={(e) => setQty(e.target.value)} onBlur={() => onPatch({ confirmedQuantity: Number(qty) || 0 })} />
              <span>×</span>
              <input className="input icap-cost" inputMode="decimal" value={cost} onChange={(e) => setCost(e.target.value)} onBlur={() => onPatch({ confirmedUnitCostCents: Math.round((Number(cost) || 0) * 100) })} />
            </>
          ) : <span>{qty} × {icapMoney(Math.round((Number(cost) || 0) * 100))}</span>}
        </span>
        <span className="icap-linecard-total">{icapMoney(lineTotalCents)}</span>
        <IcapConfBadge confidence={line.confidence} />
        {editable && (
          <label className="icap-gstfree">
            <input type="checkbox" checked={gstFree} onChange={(e) => { setGstFree(e.target.checked); onPatch({ confirmedGstFree: e.target.checked }); }} />
            GST-free
          </label>
        )}
      </div>
      <div className="icap-linecard-match">
        {line.matchedProductId ? (
          <span className="icap-match icap-match--ok">
            <OIcon name="check-circle" width={13} height={13} /> Matched{matchedSuggestion ? ": " + matchedSuggestion.name : ""}
          </span>
        ) : line.suggestedMatches && line.suggestedMatches.length > 0 ? (
          <span className="icap-match icap-match--suggest">
            Possible match:
            {line.suggestedMatches.slice(0, 3).map((s) => (
              <button key={s.productId} className="btn btn--ghost btn--sm" disabled={!editable} onClick={() => onPatch({ matchedProductId: s.productId })}>
                {s.name} ({Math.round(s.score * 100)}%)
              </button>
            ))}
          </span>
        ) : (
          <span className="icap-match icap-match--none"><OIcon name="barcode" width={13} height={13} /> No product match — goes to the Barcode queue after receiving</span>
        )}
        {line.supplierCodeRaw && <span className="icap-linecard-code">code {line.supplierCodeRaw}</span>}
      </div>
    </div>
  );
}

const ICAP_CHECK_LABELS = [
  ["totalsBalance", "Totals balance"],
  ["gst", "GST"],
  ["duplicate", "Duplicate check"],
  ["purchaseOrder", "Purchase order"],
  ["goodsReceipt", "Goods receipt"],
];

function IcapConfirmScreen() {
  const { screenState, setScreen, pushToast } = window.useOffice();
  const captureId = screenState.invoiceCaptureId;
  const [cap, setCap] = icapUseState(null);
  const [err, setErr] = icapUseState(null);
  const [suppliers, setSuppliers] = icapUseState([]);
  const [fileUrl, setFileUrl] = icapUseState(null);
  const [fileIsPdf, setFileIsPdf] = icapUseState(true);
  const [busy, setBusy] = icapUseState(false);
  const [rejecting, setRejecting] = icapUseState(false);
  const [rejectReason, setRejectReason] = icapUseState("");

  const load = icapUseCallback(() => {
    if (!captureId) return;
    window.OfficeAPI.getInvoiceCapture(captureId).then((c) => { setCap(c); setErr(null); }).catch((e) => setErr(String((e && e.message) || e)));
  }, [captureId]);
  icapUseEffect(() => { load(); }, [load]);
  icapUseEffect(() => { window.OfficeAPI.listSuppliers().then(setSuppliers).catch(() => {}); }, []);
  icapUseEffect(() => {
    if (!captureId) return;
    let alive = true, url = null;
    window.OfficeAPI.invoiceCaptureFile(captureId)
      .then((blob) => {
        if (!alive) return;
        url = URL.createObjectURL(blob);
        setFileIsPdf(blob.type === "application/pdf");
        setFileUrl(url);
      })
      .catch(() => {});
    return () => { alive = false; if (url) URL.revokeObjectURL(url); };
  }, [captureId]);

  if (!captureId) {
    return <Screen eyebrow="Buying · invoice capture" title="Confirm invoice"><div className="off-empty">No capture selected — open one from the Capture queue.</div></Screen>;
  }
  if (err) return <Screen eyebrow="Buying · invoice capture" title="Confirm invoice"><div className="off-empty"><OIcon name="alert" /> {err}</div></Screen>;
  if (!cap) return <Screen eyebrow="Buying · invoice capture" title="Confirm invoice"><div className="off-empty"><span className="spinner" /> Loading…</div></Screen>;

  const editable = cap.status === "review";
  const pendingCount = cap.lines.filter((l) => l.status !== "confirmed").length;
  const checks = cap.checks || {};

  function patchField(patch) {
    return window.OfficeAPI.patchInvoiceCapture(cap.id, patch).then(load)
      .catch((e) => pushToast({ kind: "hold", icon: "alert", title: "Couldn't save", meta: String((e && e.message) || e) }));
  }
  function patchLine(lineId, patch) {
    return window.OfficeAPI.patchInvoiceCaptureLine(lineId, patch).then(load)
      .catch((e) => pushToast({ kind: "hold", icon: "alert", title: "Couldn't save line", meta: String((e && e.message) || e) }));
  }
  function doPost() {
    if (busy) return;
    setBusy(true);
    window.OfficeAPI.postInvoiceCapture(cap.id)
      .then((r) => {
        setBusy(false);
        pushToast({
          kind: "paid", icon: "check-circle", title: "Posted",
          meta: r.status === "review" ? "Landed in Receiving, needs attention (totals didn't quite reconcile)" : "In Pending — ready to receive",
        });
        setScreen("invoiceCaptureQueue");
      })
      .catch((e) => { setBusy(false); pushToast({ kind: "hold", icon: "alert", title: "Couldn't post", meta: String((e && e.message) || e) }); });
  }
  function doSaveDraft() {
    setBusy(true);
    window.OfficeAPI.saveInvoiceCaptureDraft(cap.id)
      .then(() => { setBusy(false); pushToast({ kind: "paid", icon: "check", title: "Draft saved" }); load(); })
      .catch((e) => { setBusy(false); pushToast({ kind: "hold", icon: "alert", title: "Couldn't save", meta: String((e && e.message) || e) }); });
  }
  function doReject() {
    setBusy(true);
    window.OfficeAPI.rejectInvoiceCapture(cap.id, rejectReason)
      .then(() => { setBusy(false); pushToast({ kind: "paid", icon: "check", title: "Rejected" }); setScreen("invoiceCaptureQueue"); })
      .catch((e) => { setBusy(false); pushToast({ kind: "hold", icon: "alert", title: "Couldn't reject", meta: String((e && e.message) || e) }); });
  }

  return (
    <Screen
      eyebrow="Buying · invoice capture"
      title={cap.originalFilename}
      actions={<button className="btn btn--ghost btn--sm" onClick={() => setScreen("invoiceCaptureQueue")}><OIcon name="chevron-left" /> Back to queue</button>}
    >
      <div className="icap-confirm">
        <div className="icap-confirm-image">
          {fileUrl && fileIsPdf && <embed src={fileUrl} type="application/pdf" className="icap-confirm-embed" />}
          {fileUrl && !fileIsPdf && <img src={fileUrl} className="icap-confirm-img" alt="Original invoice" />}
          {!fileUrl && <div className="off-empty"><span className="spinner" /> Loading the document…</div>}
        </div>

        <div className="icap-confirm-fields">
          {cap.status !== "review" && (
            <div className="icap-status-banner">
              {cap.status === "posted" && <>Posted{cap.confirmedBy ? " by " + cap.confirmedBy : ""}.</>}
              {cap.status === "rejected" && <>Rejected{cap.rejectReason ? ": " + cap.rejectReason : ""}.</>}
            </div>
          )}
          {cap.kind === "service" && (
            <div className="icap-service-banner">
              <OIcon name="info" /> This looks like a service invoice — captured for the record, but not posted (Office has no ledger to code it to).
              {editable && <button className="btn btn--ghost btn--sm" onClick={() => patchField({ kind: "stock" })}>Actually, it's a stock invoice</button>}
            </div>
          )}

          <div className="icap-header-grid">
            <IcapField label="Supplier" confidence={cap.fieldConfidence.supplier} reason={cap.fieldNotes.supplier}>
              {editable ? (
                <select className="input select" value={cap.supplierId || ""} onChange={(e) => patchField({ supplierId: e.target.value || null })}>
                  <option value="">{cap.supplierNameRaw ? `"${cap.supplierNameRaw}" — choose…` : "Choose…"}</option>
                  {suppliers.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                </select>
              ) : <span>{cap.supplierName || cap.supplierNameRaw || "—"}</span>}
            </IcapField>
            <IcapField label="ABN" confidence={cap.fieldConfidence.supplier}>
              <span className="icap-static">{cap.supplierAbnRaw || "—"}</span>
            </IcapField>
            <IcapField label="Invoice #" confidence={cap.fieldConfidence.invoiceNumber} reason={cap.fieldNotes.invoiceNumber}>
              {editable ? (
                <input className="input" defaultValue={cap.invoiceNumber || ""} onBlur={(e) => e.target.value !== (cap.invoiceNumber || "") && patchField({ invoiceNumber: e.target.value })} />
              ) : <span>{cap.invoiceNumber || "—"}</span>}
            </IcapField>
            <IcapField label="Invoice date" confidence={cap.fieldConfidence.invoiceDate} reason={cap.fieldNotes.invoiceDate}>
              {editable ? <input type="date" className="input" value={cap.invoiceDate || ""} onChange={(e) => patchField({ invoiceDate: e.target.value })} /> : <span>{icapDate(cap.invoiceDate)}</span>}
            </IcapField>
            <IcapField label="Due date" confidence={cap.fieldConfidence.dueDate} reason={cap.fieldNotes.dueDate}>
              {editable ? <input type="date" className="input" value={cap.dueDate || ""} onChange={(e) => patchField({ dueDate: e.target.value || null })} /> : <span>{icapDate(cap.dueDate)}</span>}
            </IcapField>
            <IcapField label="PO reference" confidence={cap.fieldConfidence.poReference} reason={cap.fieldNotes.poReference}>
              {editable ? (
                <input className="input" defaultValue={cap.poReference || ""} onBlur={(e) => e.target.value !== (cap.poReference || "") && patchField({ poReference: e.target.value || null })} />
              ) : <span>{cap.poReference || "—"}</span>}
            </IcapField>
            <IcapField label="Freight (ex GST)" confidence={cap.fieldConfidence.freight} reason={cap.fieldNotes.freight}>
              {editable ? <IcapMoneyInput cents={cap.freightCents} onCommit={(c) => patchField({ freightCents: c })} /> : <span>{icapMoney(cap.freightCents)}</span>}
            </IcapField>
            <IcapField label="GST" confidence={cap.fieldConfidence.gst} reason={cap.fieldNotes.gst}>
              {editable ? <IcapMoneyInput cents={cap.gstCents} onCommit={(c) => patchField({ gstCents: c })} /> : <span>{icapMoney(cap.gstCents)}</span>}
            </IcapField>
            <IcapField label="Total (inc GST)" confidence={cap.fieldConfidence.total} reason={cap.fieldNotes.total}>
              {editable ? <IcapMoneyInput cents={cap.totalCents} onCommit={(c) => patchField({ totalCents: c })} /> : <span className="icap-total-value">{icapMoney(cap.totalCents)}</span>}
            </IcapField>
          </div>

          <div className="icap-lines-head">
            <h4>Lines</h4>
            <span className="icap-lines-count">{cap.lines.length} line{cap.lines.length === 1 ? "" : "s"}{pendingCount > 0 ? " · " + pendingCount + " to confirm" : ""}</span>
          </div>
          <div className="icap-lines">
            {cap.lines.map((l) => <IcapLineCard key={l.id} line={l} editable={editable} onPatch={(patch) => patchLine(l.id, patch)} />)}
            {cap.lines.length === 0 && <div className="off-empty">No lines were read from this document.</div>}
          </div>
        </div>
      </div>

      <div className="icap-checksbar">
        <div className="icap-checks">
          {ICAP_CHECK_LABELS.map(([key, label]) => {
            const chk = checks[key];
            if (!chk) return null;
            return (
              <span key={key} className={"icap-check icap-check--" + (!chk.applicable ? "na" : chk.ok ? "ok" : "bad")} title={chk.detail}>
                <OIcon name={!chk.applicable ? "info" : chk.ok ? "check-circle" : "alert"} width={14} height={14} />
                {label}
              </span>
            );
          })}
        </div>
        {editable && (
          <div className="icap-checksbar-actions">
            <span className="icap-post-note">{pendingCount > 0 ? pendingCount + " line" + (pendingCount === 1 ? "" : "s") + " left to confirm" : "All lines confirmed"}</span>
            <button className="btn btn--ghost btn--sm" onClick={() => setRejecting(true)} disabled={busy}>Reject</button>
            <button className="btn btn--ghost btn--sm" onClick={doSaveDraft} disabled={busy}>Save draft</button>
            <button className="btn btn--primary btn--sm" onClick={doPost} disabled={busy || cap.kind === "service"} title={cap.kind === "service" ? "Service invoices aren't posted — Office has no ledger for them" : undefined}>
              {busy ? <span className="spinner" /> : <OIcon name="check" />} Post
            </button>
          </div>
        )}
      </div>

      {rejecting && (
        <div className="pd-modal-back" onClick={() => setRejecting(false)}>
          <div className="pd-modal" style={{ width: 420 }} onClick={(e) => e.stopPropagation()}>
            <header className="pd-modal-head"><h3>Reject this capture</h3><button className="btn btn--ghost btn--sm" onClick={() => setRejecting(false)}><OIcon name="x" /></button></header>
            <div className="pd-modal-body">
              <div className="field">
                <label className="field-label">Why? (optional — helps whoever uploaded it)</label>
                <textarea className="input" rows={3} value={rejectReason} onChange={(e) => setRejectReason(e.target.value)} />
              </div>
            </div>
            <footer className="pd-modal-foot">
              <button className="btn btn--ghost btn--sm" onClick={() => setRejecting(false)}>Cancel</button>
              <button className="btn btn--primary btn--sm" onClick={doReject} disabled={busy}>{busy ? <span className="spinner" /> : <OIcon name="trash" />} Reject</button>
            </footer>
          </div>
        </div>
      )}
    </Screen>
  );
}

window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
  invoiceCaptureUpload: IcapUploadScreen,
  invoiceCaptureQueue: IcapQueueScreen,
  invoiceCaptureConfirm: IcapConfirmScreen,
});
