// ═══════════════════════════════════════════════════════════════
// inventory-invoices.jsx — reading and matching vendor invoices (Inventory Step 6)
// ═══════════════════════════════════════════════════════════════
//
// WHAT THIS IS (in plain English):
// Its own "Invoices" tab in the Stock Room, with two independent flows:
//
//   Supplies invoices — matched against one specific Purchase Order. Pick
//   the PO (or let it auto-detect from the invoice), upload or paste the
//   invoice, and each line comes back as matched, price doesn't match the
//   PO, billed in different packaging than ordered (the dollar totals
//   agree even though the per-unit numbers don't), billed but not on the
//   PO, or on the PO but not billed yet.
//
//   Lens (IOL) invoices — matched against what was actually dispensed to a
//   patient, mostly by serial number. Catches a lens billed but never
//   implanted, billed at the wrong price, or — the STAAR case — billed
//   after it was shipped back as a return. (That last check only finds
//   something once Step 7 builds real lens-return tracking; it is wired up
//   now so nothing else needs to change when that lands.)
//
// Reading the file: a PDF's text layer is read right in the browser with
// the pdf.js reader already loaded for the app (window.loadPdfWithTimeout,
// set up in index.html) — no scanning/OCR, and nothing new added to the
// server's dependencies. A CSV works the same way via Papa Parse (already
// vendored). Anything else — or a PDF that turns out to be a scanned
// image with no text layer — falls back to pasting the invoice text by
// hand. Every line that gets read is shown for a person to check (and fix,
// if the general-purpose reader misread something) before anything is
// matched. The actual reading and matching logic lives on the server
// (invoices.js) so the same logic is used and tested the same way
// regardless of how the text got there.
//
// Nothing here is ever silent: a price that doesn't match is only updated
// in the catalog when someone clicks "Update catalog price" on that exact
// line. Every completed reconciliation is logged to the `invoices` drawer
// (dated, who did it, what matched) so there is a real record, and a
// reconciled PO gets a small badge next to its status.
// ═══════════════════════════════════════════════════════════════

// ─── Turning a file into plain text ────────────────────────────
async function invExtractPdfText(file) {
  const buf = await file.arrayBuffer();
  const pdf = await window.loadPdfWithTimeout(buf, 20000);
  let text = '';
  for (let p = 1; p <= pdf.numPages; p++) {
    const page = await pdf.getPage(p);
    const content = await page.getTextContent();
    const rowsByY = {};
    content.items.forEach((it) => {
      const y = Math.round(it.transform[5]);
      (rowsByY[y] = rowsByY[y] || []).push({ x: it.transform[4], s: it.str });
    });
    Object.keys(rowsByY).map(Number).sort((a, b) => b - a).forEach((y) => {
      const line = rowsByY[y].sort((a, b) => a.x - b.x).map((o) => o.s).join(' ').replace(/\s+/g, ' ').trim();
      if (line) text += line + '\n';
    });
  }
  if (!text.trim()) throw new Error("Couldn't find any text in that PDF — it may be a scanned image. Paste the invoice text instead.");
  return text;
}
function invExtractCsvText(file) {
  return new Promise((resolve, reject) => {
    if (!window.Papa) { reject(new Error('The CSV reader is not available — reload the page and try again.')); return; }
    window.Papa.parse(file, {
      complete: (res) => resolve((res.data || []).map((r) => (Array.isArray(r) ? r.join('  ') : String(r))).join('\n')),
      error: (err) => reject(new Error(err.message || 'Could not read that CSV file.')),
    });
  });
}
async function invReadInvoiceFile(file) {
  const name = (file.name || '').toLowerCase();
  if (name.endsWith('.pdf')) return invExtractPdfText(file);
  if (name.endsWith('.csv')) return invExtractCsvText(file);
  throw new Error('Use a PDF or CSV file, or paste the invoice text instead.');
}

// ─── Upload + paste widget (shared by both sections) ────────────
function InvInvoiceInput({ onText, busy, setBusy }) {
  const fileRef = invUseRef(null);
  const [paste, setPaste] = invUseState('');
  const [pasteOpen, setPasteOpen] = invUseState(false);
  const [err, setErr] = invUseState('');
  const handleFile = async (file) => {
    setErr(''); setBusy(true);
    try { const text = await invReadInvoiceFile(file); await onText(text, file.name); }
    catch (e) { setErr(e.message); }
    setBusy(false);
  };
  return (
    <div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        <input ref={fileRef} type="file" accept=".pdf,.csv" style={{ display: 'none' }}
          onChange={(e) => { const f = e.target.files[0]; e.target.value = ''; if (f) handleFile(f); }} />
        <button className="btn btn-outline btn-sm" disabled={busy} onClick={() => fileRef.current && fileRef.current.click()}>Upload PDF or CSV</button>
        <button className="btn btn-outline btn-sm" disabled={busy} onClick={() => setPasteOpen(!pasteOpen)}>{pasteOpen ? 'Hide paste box' : 'Paste invoice text instead'}</button>
        {busy && <span style={{ color: 'var(--gray-500)', fontSize: '0.8rem' }}>Reading…</span>}
      </div>
      {pasteOpen && (
        <div style={{ marginTop: 8 }}>
          <textarea rows={6} style={{ width: '100%' }} value={paste} onChange={(e) => setPaste(e.target.value)}
            placeholder="Paste the invoice text here (select all in the PDF or email and copy it in)…" />
          <button className="btn btn-teal btn-sm" style={{ marginTop: 6 }} disabled={!paste.trim() || busy}
            onClick={async () => { setBusy(true); setErr(''); try { await onText(paste, 'pasted text'); setPaste(''); setPasteOpen(false); } catch (e) { setErr(e.message); } setBusy(false); }}>
            Read pasted text
          </button>
        </div>
      )}
      {err && <div style={{ color: 'var(--red)', fontSize: '0.8rem', marginTop: 6 }}>⚠️ {err}</div>}
    </div>
  );
}

// ─── Editable table of the lines read from the invoice ──────────
function InvInvoiceLinesEditor({ lines, setLines, showSerial }) {
  const upd = (i, patch) => setLines(lines.map((l, idx) => (idx === i ? { ...l, ...patch } : l)));
  const del = (i) => setLines(lines.filter((_, idx) => idx !== i));
  const add = () => setLines([...lines, { description: '', code: '', serial: '', qty: 1, price: 0 }]);
  return (
    <div className="card" style={{ margin: '8px 0' }}>
      <div className="tbl-wrap" style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead><tr>
            <th style={invTh}>Description</th><th style={invTh}>Code</th>
            {showSerial && <th style={invTh}>Serial</th>}
            <th style={{ ...invTh, textAlign: 'right' }}>Qty</th>
            <th style={{ ...invTh, textAlign: 'right' }}>Unit price</th>
            <th style={invTh} />
          </tr></thead>
          <tbody>
            {lines.map((l, i) => (
              <tr key={i}>
                <td style={invCell}><input type="text" value={l.description} onChange={(e) => upd(i, { description: e.target.value })} style={{ width: '100%', minWidth: 160 }} /></td>
                <td style={invCell}><input type="text" value={l.code || ''} onChange={(e) => upd(i, { code: e.target.value })} style={{ width: 90 }} /></td>
                {showSerial && <td style={invCell}><input type="text" value={l.serial || ''} onChange={(e) => upd(i, { serial: e.target.value })} style={{ width: 120 }} /></td>}
                <td style={{ ...invCell, textAlign: 'right' }}><input type="number" value={l.qty} onChange={(e) => upd(i, { qty: Number(e.target.value) || 0 })} style={{ width: 56, textAlign: 'right' }} /></td>
                <td style={{ ...invCell, textAlign: 'right' }}><input type="number" step="0.01" value={l.price} onChange={(e) => upd(i, { price: Number(e.target.value) || 0 })} style={{ width: 80, textAlign: 'right' }} /></td>
                <td style={invCell}><button className="btn btn-sm" onClick={() => del(i)} title="Remove this line">✕</button></td>
              </tr>
            ))}
            {lines.length === 0 && <tr><td colSpan={showSerial ? 6 : 5} style={{ ...invCell, textAlign: 'center', color: 'var(--gray-500)' }}>No lines yet — add one, or read a file above.</td></tr>}
          </tbody>
        </table>
      </div>
      <div style={{ padding: 8 }}><button className="btn btn-sm btn-outline" onClick={add}>+ line</button></div>
    </div>
  );
}

// ─── Result rendering ────────────────────────────────────────────
function InvReconSection({ title, color, count, children }) {
  if (!count) return null;
  return (
    <div style={{ marginTop: 12 }}>
      <div style={{ fontWeight: 700, fontSize: '0.8rem', color, textTransform: 'uppercase', letterSpacing: '.02em' }}>{title} ({count})</div>
      <div className="tbl-wrap" style={{ overflowX: 'auto' }}><table style={{ width: '100%', borderCollapse: 'collapse', marginTop: 4 }}><tbody>{children}</tbody></table></div>
    </div>
  );
}
function InvReconResultSupplies({ result, onAcceptPrice, onMarkReconciled, marking, already }) {
  const r = result;
  return (
    <div style={{ marginTop: 14 }}>
      <InvReconSection title="Matched" color="var(--green)" count={r.matched.length}>
        {r.matched.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description}</td><td style={{ ...invCell, textAlign: 'right' }}>{x.line.qty} × {invMoney(x.line.price)}</td></tr>)}
      </InvReconSection>
      <InvReconSection title="Price doesn't match the PO" color="var(--amber)" count={r.priceMismatch.length}>
        {r.priceMismatch.map((x, i) => (
          <tr key={i}>
            <td style={invCell}>{x.line.description}</td>
            <td style={invCell}>PO {invMoney(x.item.unitCost)} · Invoice {invMoney(x.line.price)}</td>
            <td style={invCell}>{onAcceptPrice && x.item.productId && <button className="btn btn-sm" onClick={() => onAcceptPrice(x.item.productId, x.line.price)}>Update catalog price</button>}</td>
          </tr>
        ))}
      </InvReconSection>
      <InvReconSection title="Different packaging — dollar totals agree" color="var(--gray-500)" count={r.packagingDiff.length}>
        {r.packagingDiff.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description}</td><td style={invCell}>Invoice {x.line.qty} × {invMoney(x.line.price)} = {invMoney(x.invoiceExtended)} · PO {x.item.qty} × {invMoney(x.item.unitCost)} = {invMoney(x.poExtended)}</td></tr>)}
      </InvReconSection>
      <InvReconSection title="Billed but not on this PO" color="var(--red)" count={r.billedNotOrdered.length}>
        {r.billedNotOrdered.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description}</td><td style={{ ...invCell, textAlign: 'right' }}>{x.line.qty} × {invMoney(x.line.price)}</td></tr>)}
      </InvReconSection>
      <InvReconSection title="On the PO but not billed yet" color="var(--gray-500)" count={r.orderedNotBilled.length}>
        {r.orderedNotBilled.map((x, i) => <tr key={i}><td style={invCell}>{x._desc}</td><td style={{ ...invCell, textAlign: 'right' }}>{x.qty} × {invMoney(x.unitCost)}</td></tr>)}
      </InvReconSection>
      {already ? <div style={{ marginTop: 12, color: 'var(--green)', fontWeight: 600, fontSize: '0.85rem' }}>✓ This PO is already marked reconciled.</div>
        : onMarkReconciled && <div style={{ marginTop: 14 }}><button className="btn btn-primary" disabled={marking} onClick={onMarkReconciled}>✓ Mark this invoice reconciled</button></div>}
    </div>
  );
}
function InvReconResultIol({ result, onAcceptPrice, onMarkReconciled, marking }) {
  const r = result;
  return (
    <div style={{ marginTop: 14 }}>
      <InvReconSection title="Matched" color="var(--green)" count={r.matched.length}>
        {r.matched.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description}{x.line.serial ? <span style={{ color: 'var(--gray-500)', fontSize: '0.76rem' }}> sn {x.line.serial}</span> : null}</td><td style={{ ...invCell, textAlign: 'right' }}>{invMoney(x.line.price)}</td></tr>)}
      </InvReconSection>
      <InvReconSection title="Price doesn't match what was dispensed" color="var(--amber)" count={r.priceMismatch.length}>
        {r.priceMismatch.map((x, i) => (
          <tr key={i}>
            <td style={invCell}>{x.line.description}{x.line.serial ? <span style={{ color: 'var(--gray-500)', fontSize: '0.76rem' }}> sn {x.line.serial}</span> : null}</td>
            <td style={invCell}>Dispensed at {invMoney(x.tx.unitCost)} · Invoice {invMoney(x.line.price)}</td>
            <td style={invCell}>{onAcceptPrice && x.tx.productId && <button className="btn btn-sm" onClick={() => onAcceptPrice(x.tx.productId, x.line.price)}>Update catalog price</button>}</td>
          </tr>
        ))}
      </InvReconSection>
      <InvReconSection title="Billed after it was shipped back" color="var(--red)" count={r.billedShippedBack.length}>
        {r.billedShippedBack.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description} <span style={{ color: 'var(--gray-500)', fontSize: '0.76rem' }}>sn {x.line.serial}</span></td><td style={{ ...invCell, textAlign: 'right' }}>{invMoney(x.line.price)}</td></tr>)}
      </InvReconSection>
      <InvReconSection title="Billed but not dispensed to any patient" color="var(--red)" count={r.billedNotDispensed.length}>
        {r.billedNotDispensed.map((x, i) => <tr key={i}><td style={invCell}>{x.line.description}{x.line.serial ? <span style={{ color: 'var(--gray-500)', fontSize: '0.76rem' }}> sn {x.line.serial}</span> : null}</td><td style={{ ...invCell, textAlign: 'right' }}>{invMoney(x.line.price)}</td></tr>)}
      </InvReconSection>
      {onMarkReconciled && <div style={{ marginTop: 14 }}><button className="btn btn-primary" disabled={marking} onClick={onMarkReconciled}>✓ Mark this invoice reconciled</button></div>}
    </div>
  );
}

// ─── Supplies invoices (vs a specific PO) ────────────────────────
function InvSuppliesInvoices({ stores, savedOrConflict, toast, me }) {
  const POS = (stores.purchase_orders.data || []).filter((p) => p.status !== 'cancelled');
  const [poId, setPoId] = invUseState('');
  const [poQuery, setPoQuery] = invUseState('');
  const [lines, setLines] = invUseState(null);
  const [meta, setMeta] = invUseState(null);
  const [fileName, setFileName] = invUseState('');
  const [busy, setBusy] = invUseState(false);
  const [result, setResult] = invUseState(null);
  const po = POS.find((p) => p.id === poId) || null;

  const onText = async (text, fname) => {
    setFileName(fname); setResult(null);
    const r = await InvApi.parseInvoiceText(text);
    setLines(r.lines); setMeta(r);
    if (!poId && r.poNumberGuess) {
      const guess = POS.find((p) => invNorm(p.number) === invNorm(r.poNumberGuess));
      if (guess) { setPoId(guess.id); toast(`Matched to PO ${guess.number} from the invoice.`); }
      else if (r.poNumberGuess) toast(`Couldn't find a PO matching "${r.poNumberGuess}" — pick one above.`, 'error');
    }
  };

  const reconcile = async () => {
    if (!po || !lines) return;
    setBusy(true);
    try { setResult(await InvApi.reconcileInvoice({ section: 'supplies', poId: po.id, lines, shippingAmount: meta && meta.shippingAmount })); }
    catch (e) { toast('⚠️ ' + e.message, 'error'); }
    setBusy(false);
  };

  const acceptPrice = async (productId, newPrice) => {
    const P = stores.products.data || [];
    const p = P.find((x) => x.id === productId); if (!p) return;
    const unitsPerPurchase = Number(p.unitsPerPurchase) || 1;
    const next = P.map((x) => (x.id === productId ? { ...x, purchasePrice: Math.round(newPrice * unitsPerPurchase * 10000) / 10000 } : x));
    if (await savedOrConflict(stores.products, next, `Updated ${p.description}'s catalog price`)) reconcile();
  };

  const markReconciled = async () => {
    if (!po || !result) return;
    setBusy(true);
    const now = new Date().toISOString();
    const rec = {
      id: invId('INV'), section: 'supplies', vendor: (meta && meta.vendor) || '', vendorName: (meta && meta.vendorName) || '',
      invoiceNumber: (meta && meta.invoiceNumber) || po.invoiceNumber || '', invoiceDate: (meta && meta.invoiceDate) || '',
      poId: po.id, poNumber: po.number, supplier: po.supplier, fileName,
      lines, shippingAmount: meta && meta.shippingAmount != null ? meta.shippingAmount : null,
      result: { matched: result.matched.length, priceMismatch: result.priceMismatch.length, packagingDiff: result.packagingDiff.length, billedNotOrdered: result.billedNotOrdered.length, orderedNotBilled: result.orderedNotBilled.length },
      createdAt: now, createdBy: (me && (me.name || me.email)) || '',
    };
    const INVS = stores.invoices.data || [];
    if (await savedOrConflict(stores.invoices, [rec, ...INVS], 'Invoice reconciled and logged')) {
      const POall = stores.purchase_orders.data || [];
      const nextPOs = POall.map((p) => (p.id === po.id ? { ...p, invoiceNumber: rec.invoiceNumber || p.invoiceNumber, reconciled: true, reconciledAt: now, reconciledInvoiceId: rec.id } : p));
      await savedOrConflict(stores.purchase_orders, nextPOs);
      setLines(null); setMeta(null); setResult(null); setPoId(''); setFileName('');
    }
    setBusy(false);
  };

  return (
    <div>
      <p style={{ fontSize: '0.85rem', color: 'var(--gray-600)', maxWidth: 720 }}>Matches a supplies invoice against one purchase order — quantity and price, line by line. Upload the invoice or paste its text; if the PO number is printed on it, it's picked automatically.</p>
      <InvField label="Which purchase order is this invoice for?">
        {po ? (
          <div className="card" style={{ margin: 0 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 10, flexWrap: 'wrap', gap: 8 }}>
              <div><strong>{po.number || '(no number)'}</strong> — {po.supplier} <InvTag color={PO_STATUS[po.status].color}>{PO_STATUS[po.status].label}</InvTag>{po.reconciled && <InvTag color="var(--green)">✓ reconciled</InvTag>}</div>
              <button className="btn btn-sm" onClick={() => { setPoId(''); setResult(null); }}>Change</button>
            </div>
          </div>
        ) : (
          <div style={{ position: 'relative', maxWidth: 420 }}>
            <input type="text" value={poQuery} onChange={(e) => setPoQuery(e.target.value)} placeholder="Search PO number or supplier…" />
            {poQuery.trim() && (
              <div className="card" style={{ margin: '4px 0 0', maxHeight: 220, overflowY: 'auto', position: 'absolute', zIndex: 5, width: '100%' }}>
                {POS.filter((p) => invNorm(p.number).includes(invNorm(poQuery)) || invNorm(p.supplier).includes(invNorm(poQuery))).slice(0, 15).map((p) => (
                  <div key={p.id} onClick={() => { setPoId(p.id); setPoQuery(''); }} style={{ padding: 8, cursor: 'pointer', borderBottom: '1px solid var(--gray-200)', fontSize: '0.85rem' }}>
                    <strong>{p.number || '(no number)'}</strong> — {p.supplier} <span style={{ color: 'var(--gray-500)' }}>({PO_STATUS[p.status].label})</span>
                  </div>
                ))}
                {POS.filter((p) => invNorm(p.number).includes(invNorm(poQuery)) || invNorm(p.supplier).includes(invNorm(poQuery))).length === 0 && <div style={{ padding: 8, color: 'var(--gray-500)', fontSize: '0.82rem' }}>No matching POs</div>}
              </div>
            )}
          </div>
        )}
      </InvField>
      <InvInvoiceInput onText={onText} busy={busy} setBusy={setBusy} />
      {fileName && <div style={{ fontSize: '0.76rem', color: 'var(--gray-500)', marginTop: 6 }}>{fileName}{meta && meta.vendorName ? ` — detected ${meta.vendorName}` : ''}{meta && meta.invoiceNumber ? `, invoice #${meta.invoiceNumber}` : ''}</div>}
      {lines && (
        <div>
          <div style={{ fontWeight: 600, marginTop: 14, fontSize: '0.85rem' }}>Lines read from the invoice — check these before matching:</div>
          <InvInvoiceLinesEditor lines={lines} setLines={setLines} showSerial={false} />
          <button className="btn btn-teal" disabled={!po || busy || !lines.length} onClick={reconcile}>Match against the PO</button>
        </div>
      )}
      {result && <InvReconResultSupplies result={result} onAcceptPrice={acceptPrice} onMarkReconciled={markReconciled} marking={busy} already={po && po.reconciled} />}
    </div>
  );
}

// ─── Lens (IOL) invoices (vs what was dispensed) ─────────────────
function InvIolInvoices({ stores, savedOrConflict, toast, me }) {
  const [lines, setLines] = invUseState(null);
  const [meta, setMeta] = invUseState(null);
  const [fileName, setFileName] = invUseState('');
  const [busy, setBusy] = invUseState(false);
  const [result, setResult] = invUseState(null);

  const onText = async (text, fname) => {
    setFileName(fname); setResult(null);
    const r = await InvApi.parseInvoiceText(text);
    setLines(r.lines); setMeta(r);
  };

  const reconcile = async () => {
    if (!lines) return;
    setBusy(true);
    try { setResult(await InvApi.reconcileInvoice({ section: 'iol', lines })); }
    catch (e) { toast('⚠️ ' + e.message, 'error'); }
    setBusy(false);
  };

  const acceptPrice = async (productId, newPrice) => {
    const P = stores.products.data || [];
    const p = P.find((x) => x.id === productId); if (!p) return;
    const unitsPerPurchase = Number(p.unitsPerPurchase) || 1;
    const next = P.map((x) => (x.id === productId ? { ...x, purchasePrice: Math.round(newPrice * unitsPerPurchase * 10000) / 10000 } : x));
    if (await savedOrConflict(stores.products, next, `Updated ${p.description}'s catalog price`)) reconcile();
  };

  const markReconciled = async () => {
    if (!result) return;
    setBusy(true);
    const now = new Date().toISOString();
    const rec = {
      id: invId('INV'), section: 'iol', vendor: (meta && meta.vendor) || '', vendorName: (meta && meta.vendorName) || '',
      invoiceNumber: (meta && meta.invoiceNumber) || '', invoiceDate: (meta && meta.invoiceDate) || '', fileName,
      lines,
      result: { matched: result.matched.length, priceMismatch: result.priceMismatch.length, billedNotDispensed: result.billedNotDispensed.length, billedShippedBack: result.billedShippedBack.length },
      createdAt: now, createdBy: (me && (me.name || me.email)) || '',
    };
    const INVS = stores.invoices.data || [];
    if (await savedOrConflict(stores.invoices, [rec, ...INVS], 'Invoice reconciled and logged')) { setLines(null); setMeta(null); setResult(null); setFileName(''); }
    setBusy(false);
  };

  return (
    <div>
      <p style={{ fontSize: '0.85rem', color: 'var(--gray-600)', maxWidth: 720 }}>Matches a lens invoice against what was actually dispensed to a patient, mostly by serial number. Catches a lens billed but never implanted, billed at the wrong price, or billed after it was shipped back.</p>
      <InvInvoiceInput onText={onText} busy={busy} setBusy={setBusy} />
      {fileName && <div style={{ fontSize: '0.76rem', color: 'var(--gray-500)', marginTop: 6 }}>{fileName}{meta && meta.vendorName ? ` — detected ${meta.vendorName}` : ''}{meta && meta.invoiceNumber ? `, invoice #${meta.invoiceNumber}` : ''}</div>}
      {lines && (
        <div>
          <div style={{ fontWeight: 600, marginTop: 14, fontSize: '0.85rem' }}>Lines read from the invoice — check these before matching:</div>
          <InvInvoiceLinesEditor lines={lines} setLines={setLines} showSerial={true} />
          <button className="btn btn-teal" disabled={busy || !lines.length} onClick={reconcile}>Match against dispensed lenses</button>
        </div>
      )}
      {result && <InvReconResultIol result={result} onAcceptPrice={acceptPrice} onMarkReconciled={markReconciled} marking={busy} />}
    </div>
  );
}

// ─── History ──────────────────────────────────────────────────────
function InvInvoiceHistory({ stores }) {
  const INVS = (stores.invoices.data || []).slice().sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
  if (!INVS.length) return <div className="card" style={{ maxWidth: 640 }}><div className="card-body" style={{ color: 'var(--gray-500)', fontSize: '0.86rem' }}>No invoices have been reconciled yet.</div></div>;
  return (
    <div className="card" style={{ margin: 0 }}>
      <div className="tbl-wrap" style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead><tr><th style={invTh}>Date</th><th style={invTh}>Type</th><th style={invTh}>Vendor</th><th style={invTh}>Invoice #</th><th style={invTh}>PO / supplier</th><th style={invTh}>Result</th><th style={invTh}>By</th></tr></thead>
          <tbody>
            {INVS.map((r) => (
              <tr key={r.id}>
                <td style={{ ...invCell, whiteSpace: 'nowrap' }}>{r.createdAt ? new Date(r.createdAt).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : ''}</td>
                <td style={invCell}>{r.section === 'iol' ? 'Lens (IOL)' : 'Supplies'}</td>
                <td style={invCell}>{r.vendorName || '—'}</td>
                <td style={invCell}>{r.invoiceNumber || '—'}</td>
                <td style={invCell}>{r.section === 'supplies' ? `${r.poNumber || ''} — ${r.supplier || ''}` : '—'}</td>
                <td style={{ ...invCell, fontSize: '0.76rem' }}>
                  {r.result && Object.entries(r.result).filter(([, v]) => v).map(([k, v]) => `${v} ${k.replace(/([A-Z])/g, ' $1').toLowerCase()}`).join(' · ') || 'no differences'}
                </td>
                <td style={{ ...invCell, fontSize: '0.76rem' }}>{r.createdBy}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ─── Top-level: the Invoices tab ─────────────────────────────────
function InvInvoices(ctx) {
  const [sub, setSub] = invUseState('supplies');
  const INVS = ctx.stores.invoices.data || [];
  return (
    <div>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
        <button className={`btn btn-sm ${sub === 'supplies' ? 'btn-teal' : 'btn-outline'}`} onClick={() => setSub('supplies')}>Supplies invoices</button>
        <button className={`btn btn-sm ${sub === 'iol' ? 'btn-teal' : 'btn-outline'}`} onClick={() => setSub('iol')}>Lens (IOL) invoices</button>
        <button className={`btn btn-sm ${sub === 'history' ? 'btn-teal' : 'btn-outline'}`} onClick={() => setSub('history')}>History ({INVS.length})</button>
      </div>
      {sub === 'supplies' && <InvSuppliesInvoices {...ctx} />}
      {sub === 'iol' && <InvIolInvoices {...ctx} />}
      {sub === 'history' && <InvInvoiceHistory {...ctx} />}
    </div>
  );
}
