// ════════════════════════════════════════════════════════════════════════
// PropMystro · M6 · pm-d-money.jsx  (faithful port of pm-expenses.jsx + pm-bank.jsx)
// EXPENSES — capture (manual / invoice drop / bulk CSV), SA105 box rollup
// (rolling 12 months), filterable ledger. Every cost is tagged to a property
// and an SA105 box at entry — tax becomes a query, not a reclassification.
// BANK — statement import (paste CSV / PDF-photo via extract-document fn),
// unmatched inbox with smart match (credits → rent payment, debits → expense,
// rest → ignore), reconciled list. Tables: expenses · bank_rows · payments.
// → window.PMExpensesHub, window.PMBankHub, window.PMExpenseCaptureModal
// ════════════════════════════════════════════════════════════════════════
(function () {
  const { useState, useEffect, useCallback, useRef } = React;

  // ── SA105 category map (verbatim from pm-money-data.jsx) ───────────────
  const ECATS = [
    { key: 'repairs', label: 'Repairs & maintenance', box: 25, boxLabel: 'Property repairs & maintenance' },
    { key: 'compliance', label: 'Compliance & certificates', box: 29, boxLabel: 'Other allowable property expenses' },
    { key: 'insurance', label: 'Insurance', box: 24, boxLabel: 'Rent, rates, insurance, ground rents' },
    { key: 'ground_service', label: 'Ground rent / service charge', box: 24, boxLabel: 'Rent, rates, insurance, ground rents' },
    { key: 'agent_fees', label: 'Letting / management fees', box: 27, boxLabel: 'Legal, management & professional fees' },
    { key: 'legal', label: 'Legal & professional', box: 27, boxLabel: 'Legal, management & professional fees' },
    { key: 'services', label: 'Services (cleaning, gardening)', box: 28, boxLabel: 'Costs of services, incl. wages' },
    { key: 'mortgage_interest', label: 'Mortgage interest', box: 26, boxLabel: 'Loan interest & finance costs', financeCost: true },
    { key: 'refurb', label: 'Refurbishment / improvement', box: 29, boxLabel: 'Other — review (may be capital)', capital: true },
    { key: 'other', label: 'Other', box: 29, boxLabel: 'Other allowable property expenses' },
  ];
  const ecat = (k) => ECATS.find(c => c.key === k) || ECATS[ECATS.length - 1];
  const catLabel = (k) => ecat(k).label;
  const sa105Box = (k) => ecat(k).box;

  const today = () => new Date().toISOString().slice(0, 10);
  const fmt = (d) => d ? new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '—';
  const gbp = (n) => '£' + Number(n || 0).toLocaleString('en-GB', { maximumFractionDigits: 2 });
  function ledgerWindow() { const to = today(); const d = new Date(); d.setFullYear(d.getFullYear() - 1); return { from: d.toISOString().slice(0, 10), to }; }

  function Spin() { return <span className="spin dark" />; }
  function Locked({ openBilling, title, blurb }) {
    return <div className="card card-pad"><div className="locked">
      <div className="lock-ico">💷</div><h3>{title}</h3><p>{blurb}</p>
      <button className="btn btn-primary btn-sm" style={{ width: 'auto' }} onClick={openBilling}>Upgrade to Professional</button>
      <div className="lock-foot">Your data stays exactly as it is — upgrading just unlocks the module.</div>
    </div></div>;
  }

  const fileToBase64 = (file) => new Promise((res, rej) => {
    const r = new FileReader();
    r.onload = () => { const s = String(r.result || ''); const i = s.indexOf(','); res(i >= 0 ? s.slice(i + 1) : s); };
    r.onerror = () => rej(new Error('Could not read file')); r.readAsDataURL(file);
  });

  // ═══ EXPENSE CAPTURE MODAL — manual / invoice / bulk CSV ═══════════════
  function ExpenseCaptureModal({ sb, props, prefill, onClose, onDone, toast }) {
    const sorted = props.slice().sort((a, b) => (a.address || '').localeCompare(b.address || ''));
    const fromBank = prefill && prefill.source === 'bank';
    const [mode, setMode] = useState('manual');
    const [extracting, setExtracting] = useState(false);
    const [extracted, setExtracted] = useState(false);
    const [extractError, setExtractError] = useState('');
    const [busy, setBusy] = useState(false);
    const [f, setF] = useState({
      property_id: (prefill && prefill.property_id) || (sorted[0] || {}).id || '',
      date: (prefill && prefill.date) || today(),
      category: (prefill && prefill.category) || 'repairs',
      amount: String((prefill && prefill.amount) || ''),
      vendor: (prefill && prefill.vendor) || '',
      description: (prefill && prefill.description) || '',
    });
    const set = (patch) => setF(s => ({ ...s, ...patch }));
    const c = ecat(f.category);

    // bulk CSV
    const [csvRows, setCsvRows] = useState(null);
    const resolveProperty = (token) => {
      const t = String(token || '').trim().toLowerCase(); if (!t) return null;
      const flat = (s) => String(s || '').toLowerCase().replace(/\s+/g, '');
      return sorted.find(p => String(p.id).toLowerCase() === t)
        || sorted.find(p => (p.address || '').toLowerCase() === t)
        || sorted.find(p => flat(p.postcode) === flat(t))
        || sorted.find(p => (p.address || '').toLowerCase().includes(t)) || null;
    };
    const resolveCategory = (token) => {
      const t = String(token || '').trim().toLowerCase(); if (!t) return 'other';
      return (ECATS.find(x => x.key === t) || ECATS.find(x => x.label.toLowerCase() === t)
        || ECATS.find(x => x.label.toLowerCase().includes(t) || t.includes(x.key)) || { key: 'other' }).key;
    };
    const parseCsv = (text) => {
      const lines = String(text || '').trim().split(/\r?\n/).filter(Boolean);
      if (lines.length < 2) { setCsvRows({ valid: [], errors: ['Need a header row plus at least one expense row.'] }); return; }
      const header = lines[0].toLowerCase().split(',').map(h => h.trim().replace(/^"|"$/g, ''));
      const col = (names) => header.findIndex(h => names.some(n => h.includes(n)));
      const di = col(['date']), pi = col(['property', 'address', 'postcode']), ci = col(['category', 'type']);
      const ai = col(['amount', 'cost', 'value']), vi = col(['vendor', 'payee', 'supplier']), ri = col(['description', 'reference', 'note', 'detail']);
      if (di < 0 || pi < 0 || ai < 0) { setCsvRows({ valid: [], errors: ['CSV needs at least date, property and amount columns.'] }); return; }
      const valid = [], errors = [];
      for (let i = 1; i < lines.length; i++) {
        const cells = (lines[i].match(/("[^"]*"|[^,]+)/g) || []).map(x => x.replace(/^"|"$/g, '').trim());
        const date = cells[di];
        const amount = Number(String(cells[ai] || '').replace(/[£,]/g, ''));
        const prop = resolveProperty(cells[pi]);
        if (!date || isNaN(amount) || amount <= 0) { errors.push('Row ' + i + ': missing or invalid date / amount'); continue; }
        if (!prop) { errors.push('Row ' + i + ': no property matches \u201c' + (cells[pi] || '') + '\u201d'); continue; }
        valid.push({ property_id: prop.id, propertyAddr: prop.address, date, category: ci >= 0 ? resolveCategory(cells[ci]) : 'other', amount, vendor: vi >= 0 ? cells[vi] : '', description: ri >= 0 ? cells[ri] : '' });
      }
      setCsvRows({ valid, errors });
    };
    const sampleCsv = () => {
      const a = (sorted[0] || {}).address || '14 Eldon Road';
      const b = (sorted[1] || sorted[0] || {}).address || '7 Carmine Court';
      return 'date,property,category,amount,vendor,description\n2026-05-04,"' + a + '",repairs,180,AquaFix Plumbing,Tap replacement\n2026-05-06,"' + b + '",compliance,85,British Gas,Gas safety CP12\n2026-05-09,"' + a + '",insurance,640,Direct Line,Landlord insurance';
    };
    const saveCsv = async () => {
      if (!csvRows || !csvRows.valid.length) return;
      setBusy(true);
      const { error } = await sb.from('expenses').insert(csvRows.valid.map(r => ({
        property_id: r.property_id, date: r.date, category: r.category, sa105_box: sa105Box(r.category),
        amount: r.amount, vendor: r.vendor || 'Unspecified', description: r.description || null, source: 'manual', status: 'reconciled',
      })));
      setBusy(false);
      if (error) return toast(error.message);
      toast(csvRows.valid.length + ' expenses imported'); if (onDone) onDone(); onClose();
    };

    // invoice drop — extract-document fn (vision), graceful fallback
    const onInvoiceFile = async (file) => {
      if (!file) return;
      setExtractError(''); setExtracting(true);
      try {
        if (!(file.type === 'application/pdf' || file.type.startsWith('image/'))) throw new Error('Please drop a PDF or a photo (JPEG / PNG) of the invoice.');
        const b64 = await fileToBase64(file);
        const { data, error } = await sb.functions.invoke('extract-document', { body: { base64: b64, mimeType: file.type, kind: 'invoice' } });
        if (error || !data || data.error) throw new Error((data && data.error) || (error && error.message) || 'no result');
        const d = data.extracted || data;
        const amt = d.amount != null ? String(d.amount).replace(/[£,]/g, '') : '';
        const catKey = (ECATS.find(x => x.key === String(d.category || '').toLowerCase()) || ECATS.find(x => String(d.category || '').toLowerCase().includes(x.key)) || {}).key;
        set({ vendor: d.vendor || f.vendor, amount: amt || f.amount, date: /^\d{4}-\d{2}-\d{2}$/.test(d.date || '') ? d.date : f.date, category: catKey || f.category, description: d.description || f.description });
        setExtracted(true);
      } catch (e) {
        setExtractError('Could not read the invoice automatically — enter the details below. [' + (e.message || e) + ']');
      } finally { setExtracting(false); }
    };

    const save = async () => {
      const amt = Number(f.amount) || 0;
      if (!f.property_id || amt <= 0) return;
      setBusy(true);
      const { data, error } = await sb.from('expenses').insert({
        property_id: f.property_id, date: f.date, category: f.category, sa105_box: sa105Box(f.category),
        amount: amt, vendor: f.vendor || 'Unspecified', description: f.description || null,
        source: fromBank ? 'bank' : (mode === 'invoice' ? 'invoice' : 'manual'), status: 'reconciled',
        bank_row_id: (prefill && prefill.bank_row_id) || null,
      }).select('id').single();
      setBusy(false);
      if (error) return toast(/expenses|feature/.test(error.message) ? 'Expenses needs the Professional plan.' : error.message);
      toast('Expense saved'); if (onDone) onDone(data ? data.id : null); onClose();
    };

    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 640 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>CAPTURE EXPENSE</div><h2>Log a cost</h2><p className="modal-sub">Tagged to property and SA105 box at entry. {fromBank ? 'Pre-filled from the matched bank line.' : 'Type it in, drop an invoice, or bulk-import a CSV.'}</p></div><button className="x" onClick={onClose}>✕</button></div>

      {!fromBank && <div className="tabs" style={{ margin: '0 24px 14px' }}>
        <button className={mode === 'manual' ? 'on' : ''} onClick={() => setMode('manual')}>Enter manually</button>
        <button className={mode === 'invoice' ? 'on' : ''} onClick={() => setMode('invoice')}>Drop invoice</button>
        <button className={mode === 'csv' ? 'on' : ''} onClick={() => setMode('csv')}>Bulk CSV</button>
      </div>}

      {mode === 'invoice' && !extracted && <div style={{ padding: '0 24px' }}>
        <label className="dropzone" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '22px 14px', cursor: 'pointer' }}>
          <input type="file" accept="image/*,.pdf" style={{ display: 'none' }} onChange={e => onInvoiceFile(e.target.files[0])} />
          {extracting ? <span className="pmd-mono">⏳ Reading invoice…</span>
            : <React.Fragment><span style={{ fontSize: 20 }}>✦</span><span style={{ fontWeight: 600 }}>Drop or choose an invoice</span><span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>Claude reads the vendor, amount, date &amp; category</span></React.Fragment>}
        </label>
        {extractError && <div className="alert alert-info" style={{ marginTop: 10 }}><span className="ic">ℹ</span><div>{extractError}</div></div>}
      </div>}
      {extracted && <div style={{ padding: '0 24px' }}><div className="alert alert-ok"><span className="ic">✓</span><div>Extracted — review the values below before saving.</div></div></div>}

      {mode === 'csv' && <div style={{ padding: '0 24px' }}>
        {!csvRows && <React.Fragment>
          <label className="dropzone" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '22px 14px', cursor: 'pointer' }}>
            <input type="file" accept=".csv,text/csv" style={{ display: 'none' }} onChange={e => { const file = e.target.files[0]; if (!file) return; const r = new FileReader(); r.onload = ev => parseCsv(ev.target.result); r.readAsText(file); }} />
            <span style={{ fontSize: 20 }}>⌰</span><span style={{ fontWeight: 600 }}>Drop or choose a CSV of expenses</span>
            <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>Columns: date, property, category, amount, vendor, description</span>
          </label>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 8, gap: 10, flexWrap: 'wrap' }}>
            <button className="btn btn-ghost btn-sm" onClick={() => parseCsv(sampleCsv())}>Preview sample rows</button>
            <span className="pmd-mono" style={{ fontSize: 10.5, color: 'var(--ink-faint)' }}>Property matches on address or postcode · category on name</span>
          </div>
        </React.Fragment>}
        {csvRows && <div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
            <strong>{csvRows.valid.length} ready{csvRows.errors.length ? ' · ' + csvRows.errors.length + ' skipped' : ''}</strong>
            <button className="btn btn-ghost btn-sm" onClick={() => setCsvRows(null)}>Choose another file</button>
          </div>
          {csvRows.valid.length > 0 && <div style={{ maxHeight: 240, overflow: 'auto', border: '1px solid var(--line)', borderRadius: 8 }}>
            <table className="prop-table"><thead><tr><th>Date</th><th>Property</th><th>Category</th><th>SA105</th><th style={{ textAlign: 'right' }}>Amount</th></tr></thead>
              <tbody>{csvRows.valid.map((r, i) => <tr key={i}><td className="pmd-mono" style={{ fontSize: 12 }}>{fmt(r.date)}</td><td>{r.propertyAddr}{r.vendor ? <div className="prop-meta">{r.vendor}</div> : null}</td><td>{catLabel(r.category)}</td><td><span className="badge none">Box {sa105Box(r.category)}</span></td><td style={{ textAlign: 'right' }}>{gbp(r.amount)}</td></tr>)}</tbody></table>
          </div>}
          {csvRows.errors.length > 0 && <div className="alert alert-err" style={{ marginTop: 10, maxHeight: 100, overflow: 'auto' }}><span className="ic">⚠</span><div>{csvRows.errors.map((e, i) => <div key={i}>{e}</div>)}</div></div>}
        </div>}
      </div>}

      {mode !== 'csv' && <React.Fragment>
        <div className="modal-form">
          <div className="field"><label>Property</label><select value={f.property_id} onChange={e => set({ property_id: e.target.value })}>{sorted.map(p => <option key={p.id} value={p.id}>{p.address}</option>)}</select></div>
          <div className="field"><label>Date</label><input type="date" value={f.date} max={today()} onChange={e => set({ date: e.target.value })} /></div>
          <div className="field"><label>Category</label><select value={f.category} onChange={e => set({ category: e.target.value })}>{ECATS.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}</select></div>
          <div className="field"><label>Amount (£)</label><input value={f.amount} onChange={e => set({ amount: e.target.value.replace(/[^\d.]/g, '') })} placeholder="0.00" /></div>
          <div className="field"><label>Vendor / payee</label><input value={f.vendor} placeholder="e.g. British Gas" onChange={e => set({ vendor: e.target.value })} /></div>
          <div className="field"><label>Reference (optional)</label><input value={f.description} placeholder="e.g. invoice no. / what for" onChange={e => set({ description: e.target.value })} /></div>
        </div>
        <div className="sa105-hint">Lands in <b>SA105 box {c.box}</b> — {c.boxLabel}.{c.financeCost ? ' Finance cost (restricted to 20% basic-rate credit).' : ''}{c.capital ? ' May be capital, not revenue — flag for the accountant.' : ''}</div>
      </React.Fragment>}

      <div className="modal-foot" style={{ justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        {mode === 'csv'
          ? <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!csvRows || !csvRows.valid.length || busy} onClick={saveCsv}>{busy ? <Spin /> : 'Import ' + (csvRows && csvRows.valid.length ? csvRows.valid.length + ' ' : '') + 'expense' + (csvRows && csvRows.valid.length === 1 ? '' : 's')}</button>
          : <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!f.property_id || !(Number(f.amount) > 0) || busy} onClick={save}>{busy ? <Spin /> : 'Save expense'}</button>}
      </div>
    </div></div>;
  }

  // ═══ EXPENSES HUB ═══════════════════════════════════════════════════════
  function ExpensesHub({ sb, account, toast, openBilling, onOpenProperty }) {
    const entitled = account.features && account.features.expenses === true;
    const [expenses, setExpenses] = useState(null);
    const [props, setProps] = useState([]);
    const [capture, setCapture] = useState(false);
    const [filterCat, setFilterCat] = useState('all');
    const [filterProp, setFilterProp] = useState('all');

    // deep-link: Import data → "Expenses CSV" opens the capture wizard directly
    useEffect(() => {
      if (!entitled) return;
      try { if (sessionStorage.getItem('pm-intent-expense-capture')) { sessionStorage.removeItem('pm-intent-expense-capture'); setCapture(true); } } catch (e) {}
    }, [entitled]);

    const load = useCallback(async () => {
      if (!entitled) return;
      const [e, p] = await Promise.all([
        sb.from('expenses').select('*').order('date', { ascending: false }),
        sb.from('properties').select('id,address,postcode'),
      ]);
      setExpenses(e.data || []); setProps(p.data || []);
    }, [sb, entitled]);
    useEffect(() => { load(); }, [load]);

    if (!entitled) return <React.Fragment>
      <div className="pagehead"><h1>Expenses</h1><p>Every cost, tagged to a property and an SA105 box at entry.</p></div>
      <Locked openBilling={openBilling} title="Expenses is a Professional feature" blurb="Capture costs by hand, invoice-drop or bulk CSV — each tagged to its SA105 box at entry so tax time is a query, not a sort-out." />
    </React.Fragment>;
    if (expenses === null) return <div className="card card-pad"><Spin /></div>;

    const { from, to } = ledgerWindow();
    const propById = Object.fromEntries(props.map(p => [p.id, p]));
    const inWindow = expenses.filter(e => e.date >= from && e.date <= to);
    const filtered = inWindow.filter(e => (filterProp === 'all' || e.property_id === filterProp) && (filterCat === 'all' || e.category === filterCat));
    const total = filtered.reduce((s, e) => s + (Number(e.amount) || 0), 0);
    const boxes = {};
    inWindow.forEach(e => { boxes[e.sa105_box] = (boxes[e.sa105_box] || 0) + (Number(e.amount) || 0); });
    const boxList = Object.keys(boxes).map(Number).sort((a, b) => a - b);

    const del = async (e) => {
      if (!confirm('Delete this expense?')) return;
      const { error } = await sb.from('expenses').delete().eq('id', e.id);
      if (error) toast(error.message); else { toast('Expense deleted'); load(); }
    };

    return <React.Fragment>
      <div className="pagehead" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
        <div><h1>Expenses</h1><p>Every cost, tagged to a property and an SA105 box at entry. Rolling 12 months.</p></div>
        <button className="btn btn-primary btn-sm" onClick={() => setCapture(true)}>+ Capture expense</button>
      </div>

      <div className="card card-pad" style={{ marginBottom: 16 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
          <div>
            <div className="pmd-mono" style={{ fontSize: 10.5, color: 'var(--ink-faint)', letterSpacing: '.06em' }}>SA105 BREAKDOWN</div>
            <div style={{ fontSize: 13, color: 'var(--ink-soft)', marginTop: 2 }}>Allowable expenses · rolling 12 months</div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <strong style={{ fontSize: 22, lineHeight: 1 }}>{gbp(Math.round(Object.values(boxes).reduce((a, b) => a + b, 0)))}</strong>
            <div className="pmd-mono" style={{ fontSize: 10.5, color: 'var(--ink-faint)', marginTop: 2 }}>{boxList.length} box{boxList.length === 1 ? '' : 'es'} used</div>
          </div>
        </div>
        <div className="sa-boxes">
          {boxList.length === 0 && <span style={{ color: 'var(--ink-faint)', fontSize: 13 }}>No expenses in the last 12 months yet.</span>}
          {boxList.map(b => { const x = ECATS.find(c => c.box === b) || {}; return <div key={b} className="sa-box">
            <span className="pmd-mono sa-box-n">BOX {b}</span><span className="sa-box-l">{x.boxLabel || ''}</span><strong>{gbp(Math.round(boxes[b]))}</strong>
          </div>; })}
        </div>
      </div>

      <div className="pm-toolbar">
        <select className="pm-input" value={filterProp} onChange={e => setFilterProp(e.target.value)}><option value="all">All properties</option>{props.slice().sort((a, b) => (a.address || '').localeCompare(b.address || '')).map(p => <option key={p.id} value={p.id}>{p.address}</option>)}</select>
        <select className="pm-input" value={filterCat} onChange={e => setFilterCat(e.target.value)}><option value="all">All categories</option>{ECATS.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}</select>
        <span className="pmd-mono" style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--ink-faint)' }}>{filtered.length} items · {gbp(total)}</span>
      </div>

      <div className="card" style={{ overflow: 'hidden' }}>
        <div style={{ overflowX: 'auto' }}><table className="prop-table">
          <thead><tr><th>Date</th><th>Property</th><th>Vendor</th><th>Category</th><th>SA105</th><th>Source</th><th style={{ textAlign: 'right' }}>Amount</th><th></th></tr></thead>
          <tbody>
            {filtered.length === 0 && <tr><td colSpan={8} style={{ textAlign: 'center', color: 'var(--ink-faint)', padding: 24 }}>No expenses match these filters.</td></tr>}
            {filtered.map(e => { const p = propById[e.property_id]; return <tr key={e.id}>
              <td className="pmd-mono" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>{fmt(e.date)}</td>
              <td>{p ? <button className="linkbtn" onClick={() => onOpenProperty && onOpenProperty(p)}>{p.address}</button> : '—'}</td>
              <td>{e.vendor}{e.description ? <div className="prop-meta">{e.description}</div> : null}</td>
              <td>{catLabel(e.category)}</td>
              <td><span className="badge none">Box {e.sa105_box}</span></td>
              <td><span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{e.source}</span></td>
              <td style={{ textAlign: 'right', fontWeight: 600 }}>{gbp(e.amount)}</td>
              <td style={{ textAlign: 'right' }}><button className="linkbtn" style={{ color: 'var(--red)' }} title="Delete" onClick={() => del(e)}>×</button></td>
            </tr>; })}
          </tbody>
        </table></div>
      </div>

      {capture && <ExpenseCaptureModal sb={sb} props={props} prefill={null} toast={toast} onClose={() => setCapture(false)} onDone={() => load()} />}
    </React.Fragment>;
  }

  // ═══ BANK RECONCILIATION ════════════════════════════════════════════════
  const SAMPLE_CSV = 'date,description,amount\n' + (() => {
    const d = (o) => new Date(Date.now() - o * 864e5).toISOString().slice(0, 10);
    return [d(2) + ',"FASTER PYMT PATEL J",2450.00', d(3) + ',"DD BRITISH GAS BUS",85.00', d(5) + ',"FASTER PYMT ABOUD M",1600.00', d(6) + ',"CARD SCREWFIX 4412",36.50', d(9) + ',"DD DIRECT LINE LANDLORD",53.80', d(11) + ',"TFR SAVINGS",500.00'].join('\n');
  })();

  function suggestMatch(row, tenancies, tenants) {
    if (row.amount > 0) {
      let best = null, bestDiff = Infinity;
      for (const ty of tenancies) {
        if (ty.status !== 'active') continue;
        const diff = Math.abs((Number(ty.rent_pcm) || 0) - row.amount);
        const lead = tenants.find(t => t.id === ty.lead_tenant_id);
        const nameHit = lead && row.description && row.description.toUpperCase().includes((lead.full_name || '').split(' ').slice(-1)[0].toUpperCase());
        const score = nameHit ? diff - 5000 : diff;
        if (score < bestDiff) { bestDiff = score; best = ty; }
      }
      if (best) return { type: 'rent', tenancy: best, confident: bestDiff < 0 || bestDiff <= 50 };
      return null;
    }
    const d = (row.description || '').toLowerCase();
    let cat = 'other';
    if (/insur/.test(d)) cat = 'insurance';
    else if (/gas|eicr|epc|safety|electr|cert/.test(d)) cat = 'compliance';
    else if (/plumb|repair|screwfix|boiler|build|roof|damp|mould/.test(d)) cat = 'repairs';
    else if (/letting|manage|agent/.test(d)) cat = 'agent_fees';
    else if (/legal|solicit|account/.test(d)) cat = 'legal';
    else if (/clean|garden/.test(d)) cat = 'services';
    else if (/service charge|ground rent/.test(d)) cat = 'ground_service';
    else if (/mortgage|interest/.test(d)) cat = 'mortgage_interest';
    return { type: 'expense', category: cat };
  }

  function RecordPaymentModal({ sb, tenancy, property, lead, prefill, onClose, onDone, toast }) {
    const [f, setF] = useState({ date: prefill.date || today(), amount: String(prefill.amount || tenancy.rent_pcm || ''), method: 'bank_transfer', ref: prefill.ref || '' });
    const [busy, setBusy] = useState(false);
    const save = async () => {
      setBusy(true);
      const { error } = await sb.from('payments').insert({
        tenancy_id: tenancy.id, property_id: tenancy.property_id, date: f.date, amount: Number(f.amount) || 0,
        kind: 'payment', method: f.method, ref: f.ref || null, bank_row_id: prefill.bank_row_id || null,
      });
      setBusy(false);
      if (error) return toast(/ledger|feature/.test(error.message) ? 'The rent ledger needs the Professional plan.' : error.message);
      toast('Payment recorded'); onDone();
    };
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 460 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>RECORD PAYMENT</div><h2>{lead ? lead.full_name : 'Rent'}{property ? ' · ' + property.address : ''}</h2></div><button className="x" onClick={onClose}>✕</button></div>
      <div className="modal-form">
        <div className="field"><label>Date</label><input type="date" value={f.date} onChange={e => setF({ ...f, date: e.target.value })} /></div>
        <div className="field"><label>Amount (£)</label><input value={f.amount} onChange={e => setF({ ...f, amount: e.target.value.replace(/[^\d.]/g, '') })} /></div>
        <div className="field full"><label>Reference</label><input value={f.ref} onChange={e => setF({ ...f, ref: e.target.value })} /></div>
      </div>
      <div className="modal-foot" style={{ justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" style={{ width: 'auto' }} disabled={busy || !(Number(f.amount) > 0)} onClick={save}>{busy ? <Spin /> : 'Record payment'}</button>
      </div>
    </div></div>;
  }

  function BankRow({ sb, row, props, tenancies, tenants, reload, toast }) {
    const suggestion = suggestMatch(row, tenancies, tenants);
    const active = tenancies.filter(t => t.status === 'active');
    const [tenancyId, setTenancyId] = useState(suggestion && suggestion.tenancy ? suggestion.tenancy.id : (active[0] || {}).id);
    const [modal, setModal] = useState(null);
    const isCredit = row.amount > 0;
    const ten = tenancies.find(t => t.id === tenancyId);
    const property = ten && props.find(p => p.id === ten.property_id);
    const lead = ten && tenants.find(t => t.id === ten.lead_tenant_id);

    const mark = async (matchType, matchLabel) => {
      const { error } = await sb.from('bank_rows').update({ status: 'matched', match_type: matchType, match_label: matchLabel }).eq('id', row.id);
      if (error) toast(error.message); else reload();
    };
    const ignore = async () => {
      const { error } = await sb.from('bank_rows').update({ status: 'ignored', match_label: 'Ignored' }).eq('id', row.id);
      if (error) toast(error.message); else reload();
    };

    return <div className="bank-row">
      <div className="bank-row-main">
        <span className="pmd-mono bank-date">{fmt(row.date)}</span>
        <span className="bank-desc">{row.description}</span>
        <span className={'bank-amt ' + (isCredit ? 'credit' : 'debit')}>{isCredit ? '+' : '−'}{gbp(Math.abs(row.amount))}</span>
      </div>
      <div className="bank-row-match">
        {isCredit ? <React.Fragment>
          <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>rent →</span>
          <select className="pm-input" style={{ maxWidth: 260 }} value={tenancyId || ''} onChange={e => setTenancyId(e.target.value)}>
            {active.map(t => { const l = tenants.find(x => x.id === t.lead_tenant_id); const p = props.find(x => x.id === t.property_id); return <option key={t.id} value={t.id}>{(l ? l.full_name : '—') + ' · ' + (p ? p.address : '')}</option>; })}
          </select>
          <button className="btn btn-primary btn-sm" disabled={!ten} onClick={() => setModal('pay')}>Record payment</button>
        </React.Fragment> : <React.Fragment>
          <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>expense →</span>
          <button className="btn btn-primary btn-sm" onClick={() => setModal('expense')}>Capture expense</button>
        </React.Fragment>}
        <button className="btn btn-ghost btn-sm" onClick={ignore} title="Personal transfer / not a property transaction">Ignore</button>
      </div>
      {modal === 'pay' && ten && <RecordPaymentModal sb={sb} tenancy={ten} property={property} lead={lead} toast={toast}
        prefill={{ amount: Math.abs(row.amount), date: row.date, ref: row.description, bank_row_id: row.id }}
        onDone={() => { setModal(null); mark('rent', (lead ? lead.full_name : 'Rent') + (property ? ' · ' + property.address : '')); }} onClose={() => setModal(null)} />}
      {modal === 'expense' && <ExpenseCaptureModal sb={sb} props={props} toast={toast}
        prefill={{ source: 'bank', amount: Math.abs(row.amount), date: row.date, vendor: row.description, category: (suggestion && suggestion.category) || 'other', bank_row_id: row.id }}
        onDone={() => { setModal(null); mark('expense', 'Expense'); }} onClose={() => setModal(null)} />}
    </div>;
  }

  function BankImportModal({ sb, onClose, reload, toast }) {
    const [text, setText] = useState('');
    const [error, setError] = useState('');
    const [busy, setBusy] = useState(false);
    const parse = async () => {
      const lines = text.trim().split(/\r?\n/).filter(Boolean);
      if (lines.length < 2) return setError('Need a header row plus at least one transaction.');
      const header = lines[0].toLowerCase();
      const di = header.split(',').findIndex(h => /date/.test(h));
      const ai = header.split(',').findIndex(h => /amount|value/.test(h));
      if (di < 0 || ai < 0) return setError('Could not find date and amount columns.');
      const rows = [];
      for (let i = 1; i < lines.length; i++) {
        const cells = (lines[i].match(/("[^"]*"|[^,]+)/g) || []).map(c => c.replace(/^"|"$/g, '').trim());
        const date = cells[di];
        const amount = Number(String(cells[ai]).replace(/[£,]/g, ''));
        const description = cells.filter((_, idx) => idx !== di && idx !== ai).join(' ') || 'Transaction';
        if (!date || isNaN(amount)) continue;
        rows.push({ date, description, amount, account: 'main', status: 'unmatched' });
      }
      if (!rows.length) return setError('No valid transactions parsed.');
      setBusy(true);
      const { error: err } = await sb.from('bank_rows').insert(rows);
      setBusy(false);
      if (err) return setError(/bank|feature/.test(err.message) ? 'Bank reconciliation needs the Professional plan.' : err.message);
      toast(rows.length + ' transactions imported'); reload(); onClose();
    };
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 620 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>IMPORT STATEMENT</div><h2>Paste a bank statement</h2><p className="modal-sub">CSV with <code>date, description, amount</code> columns — credits positive, debits negative. Most UK bank exports work as-is.</p></div><button className="x" onClick={onClose}>✕</button></div>
      <div style={{ padding: '0 24px' }}>
        <textarea rows={8} style={{ width: '100%', fontFamily: 'JetBrains Mono, monospace', fontSize: 12, padding: 10, border: '1.5px solid var(--line)', borderRadius: 'var(--radius-sm)' }} value={text} placeholder={'date,description,amount\n2026-05-02,"FASTER PYMT ABOUD M",1000.00'} onChange={e => { setText(e.target.value); setError(''); }}></textarea>
        {error && <div className="alert alert-err" style={{ marginTop: 10 }}><span className="ic">⚠</span><div>{error}</div></div>}
      </div>
      <div className="modal-foot" style={{ justifyContent: 'space-between', gap: 8 }}>
        <button className="btn btn-ghost btn-sm" onClick={() => { setText(SAMPLE_CSV); setError(''); }}>Load sample rows</button>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!text.trim() || busy} onClick={parse}>{busy ? <Spin /> : 'Import'}</button>
        </div>
      </div>
    </div></div>;
  }

  function BankDropModal({ sb, onClose, reload, toast }) {
    const [phase, setPhase] = useState('drop');
    const [rows, setRows] = useState([]);
    const [error, setError] = useState('');
    const [fileName, setFileName] = useState('');
    const [busy, setBusy] = useState(false);
    const fileRef = useRef(null);
    const handleFile = async (file) => {
      if (!file) return;
      setError(''); setFileName(file.name); setPhase('extracting');
      try {
        if (!(file.type === 'application/pdf' || file.type.startsWith('image/'))) throw new Error('Please upload a PDF or a photo (JPEG / PNG) of your statement.');
        const b64 = await fileToBase64(file);
        const { data, error: err } = await sb.functions.invoke('extract-document', { body: { base64: b64, mimeType: file.type, kind: 'bank_statement' } });
        if (err || !data || data.error) throw new Error((data && data.error) || (err && err.message) || 'no result');
        const transactions = data.transactions || data.extracted;
        if (!Array.isArray(transactions) || transactions.length === 0) throw new Error('No transactions found. Try a cleaner scan or use CSV import.');
        setRows(transactions.map((t, i) => ({ date: t.date || '', description: t.description || '', amount: Number(t.amount) || 0, include: true, _k: 'r' + i })));
        setPhase('review');
      } catch (e) { setError('Could not read the statement automatically — use CSV import instead. [' + (e.message || e) + ']'); setPhase('drop'); }
    };
    const doImport = async () => {
      setBusy(true);
      const toImport = rows.filter(r => r.include).map(r => ({ date: r.date, description: r.description, amount: r.amount, account: 'main', status: 'unmatched' }));
      const { error: err } = await sb.from('bank_rows').insert(toImport);
      setBusy(false);
      if (err) return setError(err.message);
      toast(toImport.length + ' transactions imported'); reload(); onClose();
    };
    return <div className="modal-scrim" onClick={phase === 'extracting' ? undefined : onClose}><div className="modal" style={{ maxWidth: 680 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>SMART STATEMENT IMPORT</div><h2>Drop a bank statement</h2>{phase === 'drop' && <p className="modal-sub">PDF or a photo of your statement — Claude reads the transactions straight into the reconciliation inbox.</p>}</div>{phase !== 'extracting' && <button className="x" onClick={onClose}>✕</button>}</div>
      {phase === 'drop' && <div style={{ padding: '0 24px' }}>
        <div className="dropzone" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '26px 14px', cursor: 'pointer' }}
          onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); }} onClick={() => fileRef.current && fileRef.current.click()}>
          <span style={{ fontSize: 22 }}>📄</span><span style={{ fontWeight: 600 }}>Drop PDF or photo here, or click to upload</span>
          <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>NatWest · Barclays · HSBC · Lloyds · Monzo · any UK bank</span>
          <input ref={fileRef} type="file" accept=".pdf,image/*" style={{ display: 'none' }} onChange={e => handleFile(e.target.files[0])} />
        </div>
        {error && <div className="alert alert-info" style={{ marginTop: 10 }}><span className="ic">ℹ</span><div>{error}</div></div>}
      </div>}
      {phase === 'extracting' && <div style={{ textAlign: 'center', padding: '30px 0' }}><Spin /><div style={{ marginTop: 12, fontWeight: 600 }}>Reading {fileName}…</div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', marginTop: 4 }}>Claude is extracting your transactions</div></div>}
      {phase === 'review' && <div style={{ padding: '0 24px' }}>
        <p className="modal-sub"><b>{rows.filter(r => r.include).length}</b> of {rows.length} transactions selected. Uncheck anything that isn't a property transaction.</p>
        <div style={{ maxHeight: 320, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 8, marginTop: 8 }}>
          <table className="prop-table"><thead><tr><th style={{ width: 28 }}></th><th>Date</th><th>Description</th><th style={{ textAlign: 'right' }}>Amount</th></tr></thead>
            <tbody>{rows.map((r, i) => <tr key={r._k} style={{ opacity: r.include ? 1 : 0.4 }}>
              <td style={{ textAlign: 'center' }}><input type="checkbox" checked={r.include} onChange={e => setRows(rows.map((x, j) => j === i ? { ...x, include: e.target.checked } : x))} /></td>
              <td className="pmd-mono" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>{r.date}</td>
              <td><input className="pm-input" style={{ width: '100%' }} value={r.description} onChange={e => setRows(rows.map((x, j) => j === i ? { ...x, description: e.target.value } : x))} /></td>
              <td style={{ textAlign: 'right', fontWeight: 600, color: r.amount >= 0 ? 'var(--ok)' : 'var(--red)' }}>{r.amount >= 0 ? '+' : '−'}{gbp(Math.abs(r.amount))}</td>
            </tr>)}</tbody></table>
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', margin: '12px 0 0' }}>
          <button className="btn btn-ghost btn-sm" onClick={() => { setPhase('drop'); setError(''); }}>← Try another file</button>
          <button className="btn btn-primary btn-sm" disabled={!rows.some(r => r.include) || busy} onClick={doImport}>{busy ? <Spin /> : 'Import ' + rows.filter(r => r.include).length + ' transactions →'}</button>
        </div>
      </div>}
      <div style={{ height: 18 }}></div>
    </div></div>;
  }

  function BankHub({ sb, account, toast, openBilling }) {
    const entitled = account.features && account.features.bank === true;
    const [bankRows, setBankRows] = useState(null);
    const [props, setProps] = useState([]);
    const [tenancies, setTenancies] = useState([]);
    const [tenants, setTenants] = useState([]);
    const [importing, setImporting] = useState(null);
    const [showRec, setShowRec] = useState(false);
    const [conns, setConns] = useState([]);
    const [connecting, setConnecting] = useState(false);
    const [syncing, setSyncing] = useState(false);

    const load = useCallback(async () => {
      if (!entitled) return;
      const [b, p, ty, tn, bc] = await Promise.all([
        sb.from('bank_rows').select('*').order('date', { ascending: false }),
        sb.from('properties').select('id,address,postcode'),
        sb.from('tenancies').select('*'),
        sb.from('tenants').select('id,full_name'),
        sb.from('bank_connections').select('*').neq('status', 'revoked').order('created_at', { ascending: false }),
      ]);
      setBankRows(b.data || []); setProps(p.data || []); setTenancies(ty.data || []); setTenants(tn.data || []);
      setConns(bc.error ? [] : (bc.data || []));   // table may not exist until openbanking.sql is run
    }, [sb, entitled]);
    useEffect(() => { load(); }, [load]);

    // Returning from the bank's authorisation page. Enable Banking forbids query
    // params in the *registered* redirect, so we register a bare URL (e.g.
    // https://app.propmystro.com/) and the bank appends ?code=<code>&state=<id>
    // itself on the way back — that code+state pair IS the return signal.
    useEffect(() => {
      if (!entitled) return;
      const qs = new URLSearchParams(location.search);
      const code = qs.get('code');
      const ref = qs.get('state') || qs.get('ref');   // state carries our connection id (ref kept for back-compat)
      if (!code || !ref) return;
      history.replaceState({}, '', location.pathname);
      (async () => {
        toast('Finalising the bank link…');
        try {
          const { data, error } = await sb.functions.invoke('bank-link', { body: { action: 'finalize', connection_id: ref, code } });
          if (error) throw new Error(await fnErr(error));
          if (data && data.error) throw new Error(data.error);
          if (data.status === 'linked') {
            toast('Bank linked — pulling transactions…');
            await sb.functions.invoke('bank-sync', { body: {} });
            toast('Bank linked ✓ — new transactions are in the inbox');
          } else toast('Bank link ' + data.status + ' — try connecting again.');
        } catch (e) { toast(e.message); }
        load();
      })();
    }, [entitled]);   // eslint-disable-line

    const syncNow = async () => {
      setSyncing(true);
      try {
        const { data, error } = await sb.functions.invoke('bank-sync', { body: {} });
        if (error) throw new Error(error.message);
        if (data && data.error) throw new Error(data.error);
        toast((data.results && data.results.join(' · ')) || 'Synced');
        load();
      } catch (e) { toast(/not configured/.test(e.message) ? 'Open Banking isn\u2019t configured yet — see supabase/openbanking-setup-guide.md' : e.message); }
      setSyncing(false);
    };
    const disconnect = async (conn) => {
      if (!confirm('Disconnect ' + (conn.institution_name || 'this bank') + '? Existing transactions stay; no new ones will sync.')) return;
      const { data, error } = await sb.functions.invoke('bank-link', { body: { action: 'disconnect', connection_id: conn.id } });
      if (error || (data && data.error)) toast((data && data.error) || error.message); else { toast('Bank disconnected'); load(); }
    };

    if (!entitled) return <React.Fragment>
      <div className="pagehead"><h1>Bank reconciliation</h1><p>Match statement lines to rent and expenses.</p></div>
      <Locked openBilling={openBilling} title="Bank reconciliation is a Professional feature" blurb="Import statements (CSV, PDF or photo) and clear the inbox: credits become rent payments, debits become expenses — everything accounted for." />
    </React.Fragment>;
    if (bankRows === null) return <div className="card card-pad"><Spin /></div>;

    const inbox = bankRows.filter(r => r.status === 'unmatched');
    const reconciled = bankRows.filter(r => r.status !== 'unmatched');
    const linked = conns.find(c => c.status === 'linked');
    const expired = conns.find(c => c.status === 'expired');

    return <React.Fragment>
      <div className="pagehead" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
        <div><h1>Bank reconciliation</h1><p>Match imported statement lines to rent and expenses. Anything left in the inbox is money the books haven't accounted for.</p></div>
        <div className="summary" style={{ margin: 0 }}>
          <div className={'sumcard' + (inbox.length ? ' warn' : ' ok')}><div className="n">{inbox.length}</div><div className="l">To review</div></div>
          <div className="sumcard ok"><div className="n">{reconciled.filter(r => r.status === 'matched').length}</div><div className="l">Reconciled</div></div>
        </div>
      </div>

      <div className="pm-toolbar">
        <button className="btn btn-primary btn-sm" onClick={() => setImporting('csv')}>↑ Import statement (CSV)</button>
        <button className="btn btn-primary btn-sm" onClick={() => setImporting('vision')}>📄 Drop statement (PDF / photo)</button>
        {linked ? <React.Fragment>
          <span className="badge ok" title={(linked.bank_accounts || []).length + ' account(s) · last sync ' + (linked.last_synced_at ? new Date(linked.last_synced_at).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : 'never')}>⚡ {linked.institution_name || 'Bank'} linked</span>
          <button className="btn btn-ghost btn-sm" disabled={syncing} onClick={syncNow}>{syncing ? 'Syncing…' : '⟳ Sync now'}</button>
          <button className="btn btn-ghost btn-sm" onClick={() => disconnect(linked)}>Disconnect</button>
        </React.Fragment> : <span className="badge none" style={{ opacity: .85 }} title="Direct bank-feed sync is coming soon. For now, import statements by CSV or PDF/photo above.">⚡ Open Banking · coming soon</span>}
        <span className="pmd-mono" style={{ fontSize: 10.5, color: 'var(--ink-faint)' }}>{linked ? 'Transactions sync into this inbox automatically every morning.' : 'Automatic bank-feed sync is coming soon — for now, import statements by CSV or PDF/photo above.'}</span>
      </div>

      <div className="card" style={{ overflow: 'hidden', marginBottom: 16 }}>
        <div className="card-head"><div><h3>Unmatched</h3><div className="sub">{inbox.length} line{inbox.length === 1 ? '' : 's'} to review</div></div></div>
        {inbox.length === 0 && <div className="empty" style={{ padding: '26px 20px' }}><div className="ico">✓</div><h3>Inbox clear</h3><p>Every imported line is reconciled.</p></div>}
        {inbox.map(row => <BankRow key={row.id} sb={sb} row={row} props={props} tenancies={tenancies} tenants={tenants} reload={load} toast={toast} />)}
      </div>

      {reconciled.length > 0 && <div className="card" style={{ overflow: 'hidden' }}>
        <div className="card-head" style={{ cursor: 'pointer' }} onClick={() => setShowRec(v => !v)}>
          <div><h3>Reconciled · {reconciled.length}</h3><div className="sub">{showRec ? 'Click to collapse' : 'Click to expand'}</div></div>
        </div>
        {showRec && reconciled.map(r => <div className="row" key={r.id}>
          <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', minWidth: 90 }}>{fmt(r.date)}</span>
          <div className="main"><div className="t" style={{ fontWeight: 500, fontSize: 13.5 }}>{r.description}</div><div className="s">{r.match_label || r.status}</div></div>
          <span style={{ fontWeight: 600, color: r.amount >= 0 ? 'var(--ok)' : 'var(--ink-soft)' }}>{r.amount >= 0 ? '+' : '−'}{gbp(Math.abs(r.amount))}</span>
          <span className={'badge ' + (r.status === 'matched' ? 'ok' : 'none')}>{r.status}</span>
        </div>)}
      </div>}

      {importing === 'csv' && <BankImportModal sb={sb} onClose={() => setImporting(null)} reload={load} toast={toast} />}
      {importing === 'vision' && <BankDropModal sb={sb} onClose={() => setImporting(null)} reload={load} toast={toast} />}
      {/* Connect-bank picker parked until a UK Open Banking provider is wired (see openbanking-setup-guide.md). The ConnectBankModal + finalize handler stay ready below. */}
      {connecting && <ConnectBankModal sb={sb} onClose={() => setConnecting(false)} toast={toast} />}
    </React.Fragment>;
  }

  // ═══ CONNECT BANK — Open Banking institution picker (Enable Banking) ══
  // Supabase's functions.invoke collapses any non-2xx into a generic
  // "Edge Function returned a non-2xx status code" — dig the real {error}
  // message out of the response body so the user sees what actually failed.
  async function fnErr(error) {
    try { const b = await error.context.clone().json(); if (b && b.error) return b.error; } catch (_e) {}
    try { const t = await error.context.clone().text(); if (t) return t.slice(0, 300); } catch (_e) {}
    return error.message || 'Request failed';
  }
  function ConnectBankModal({ sb, onClose, toast }) {
    const [banks, setBanks] = useState(null);
    const [err, setErr] = useState('');
    const [q, setQ] = useState('');
    const [busy, setBusy] = useState('');

    useEffect(() => { (async () => {
      try {
        const { data, error } = await sb.functions.invoke('bank-link', { body: { action: 'institutions' } });
        if (error) throw new Error(await fnErr(error));
        if (data && data.error) throw new Error(data.error);
        setBanks(data.banks || []);
      } catch (e) {
        setErr(/not configured|Failed to send/.test(e.message) ? 'Open Banking isn\u2019t configured yet — deploy the bank-link function and set the Enable Banking secrets (see supabase/openbanking-setup-guide.md).' : e.message);
        setBanks([]);
      }
    })(); }, []);   // eslint-disable-line

    const pick = async (bank) => {
      setBusy(bank.id); setErr('');
      try {
        const redirect = location.origin + location.pathname;   // bare URL — Enable Banking rejects query params in the redirect; it appends ?code&state itself
        const { data, error } = await sb.functions.invoke('bank-link', { body: { action: 'create', institution_id: bank.id, institution_name: bank.name, institution_logo: bank.logo || '', redirect } });
        if (error) throw new Error(await fnErr(error));
        if (data && data.error) throw new Error(data.error);
        window.location.href = data.link;   // off to the bank's authorisation page
      } catch (e) { setErr(e.message); setBusy(''); }
    };

    const filtered = (banks || []).filter(b => !q || b.name.toLowerCase().includes(q.toLowerCase()));
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 560 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>OPEN BANKING · POWERED BY ENABLE BANKING</div><h2>Connect your bank</h2><p className="modal-sub">Pick your bank, authorise read-only access on their own site, and transactions flow into the reconciliation inbox automatically. You can revoke at any time.</p></div><button className="x" onClick={onClose}>✕</button></div>
      {err && <div style={{ padding: '0 24px' }}><div className="alert alert-err"><span className="ic">⚠</span><div>{err}</div></div></div>}
      <div style={{ padding: '0 24px 4px' }}>
        <input className="pm-input" style={{ width: '100%' }} placeholder="Search banks… e.g. Monzo, Barclays, HSBC" value={q} onChange={e => setQ(e.target.value)} autoFocus />
      </div>
      <div style={{ padding: '10px 24px 0', maxHeight: '46vh', overflowY: 'auto' }}>
        {banks === null ? <div style={{ padding: '20px 0' }}><Spin /></div>
          : filtered.length === 0 ? <div className="empty" style={{ padding: '20px 10px' }}><p style={{ margin: 0 }}>No banks match “{q}”.</p></div>
          : filtered.slice(0, 80).map(b => <div className="row" key={b.id} style={{ padding: '10px 2px' }}>
            {b.logo ? <img src={b.logo} alt="" style={{ width: 34, height: 34, borderRadius: 9, objectFit: 'contain', background: 'var(--surface-2)', flex: '0 0 auto' }} /> : <span className="avatar" style={{ width: 34, height: 34 }}>{(b.name || '?')[0]}</span>}
            <div className="main"><div className="t" style={{ fontSize: 14 }}>{b.name}</div></div>
            <button className="btn btn-primary btn-sm" disabled={!!busy} onClick={() => pick(b)}>{busy === b.id ? 'Opening…' : 'Connect →'}</button>
          </div>)}
      </div>
      <div className="modal-foot"><span className="foot-note">Read-only access via the FCA-regulated Enable Banking service · consent lasts ~90 days, then renew · use “Mock ASPSP” to test without a real bank.</span></div>
    </div></div>;
  }

  window.PMExpensesHub = ExpensesHub;
  window.PMBankHub = BankHub;
  window.PMExpenseCaptureModal = ExpenseCaptureModal;
})();
