// ═══════════════════════════════════════════════════ LEON BOQ
// The Gulf operation does not quote the way the US operation quotes. Their
// submittal is a BILL OF QUANTITIES, and its shape is the client's own words:
//
//   "For Abu Dhabi we need to do everything by scopes… quotation by scope. On
//    the quotations we need to have dollar and their currency as final number.
//    We don't provide any labor, not even for countertop. Their submittal of
//    quotation is by BOQ per unit category and then per scope."
//
// A "unit category" is an APARTMENT TYPE. So the bill has three levels —
// apartment type → scope → work item — and ONE rule drives all of it:
//
//   A LINE'S QUANTITY IS ENTERED FOR ONE APARTMENT OF THAT TYPE, AND THEN
//   MULTIPLIED BY HOW MANY OF THAT TYPE THE JOB HAS.
//
// That multiplication is the whole reason the bill is organised this way: a
// tower is thirty of Type A and twelve of Type B, and nobody measures thirty
// identical apartments. It is also why the multiplier is printed on the page
// next to every subtotal rather than being folded silently into a total — a
// number you cannot check is a number that gets argued about on site.
//
// NOTHING HERE IS A SECOND COPY OF A PROJECT RECORD:
//   · the arithmetic is boqTotals() in lib.jsx — there is no second engine here;
//   · the scopes are the project's own families, filtered by the CLIENT'S
//     REGION through familiesForRegion();
//   · the work items are those families' own `workItems`;
//   · the record shapes are makeBoq / makeBoqUnitType / makeBoqLine in data.jsx;
//   · the bill lives on the project as `project.boqs`, so it persists with the
//     project and this module declares no state of its own.
//
// THREE THINGS THIS FILE REFUSES TO DO, EACH FOR THE SAME REASON — a quotation
// that quietly says something untrue is worse than one that says nothing:
//   1. An unset rate is NOT zero. A line with no rate contributes nothing, is
//      counted, and the bill reports itself INCOMPLETE and names the lines.
//   2. The FX rate is entered ONCE and STAMPED with its date. It is never live.
//      A live rate silently changes a total that has already been sent, which
//      is the one thing a quotation must not do. With no rate entered, the
//      second currency is blank and says so — it is never shown as 0.00.
//   3. Supply only is STATED, not inferred. Every Abu Dhabi scope is flagged
//      supplyOnly in the scope library and the printed bill says in words that
//      no installation is included, because that is the most consequential
//      difference between this bill and a US quote.

const BOQ_TABS = [
  { key: 'types', label: 'Apartment Types', icon: '🏢' },
  { key: 'bill', label: 'The Bill', icon: '📋' },
  { key: 'money', label: 'Currency & Terms', icon: '💱' },
  { key: 'document', label: 'Document', icon: '📄' },
  { key: 'revisions', label: 'Revisions', icon: '🕘' },
];

// A take-off measures in the drawing module's vocabulary; a bill is written in
// the client's. Anything not on this list is left for a person to choose in the
// review step rather than guessed at — a wrong unit is a wrong price.
const BOQ_TAKEOFF_UNITS = {
  'SF': 'sqft', 'SQFT': 'sqft', 'SQ FT': 'sqft', 'SQ. FT.': 'sqft', 'SQUARE FEET': 'sqft',
  'SM': 'm²', 'SQM': 'm²', 'M2': 'm²', 'M²': 'm²', 'SQ. M.': 'm²', 'SQUARE METRES': 'm²',
  'LF': 'lin ft', 'LIN FT': 'lin ft', 'LIN. FT.': 'lin ft',
  'LM': 'lm', 'LIN M': 'lm', 'LIN. M.': 'lm',
  'M': 'm', 'MTR': 'm',
  'EA': 'nos', 'EACH': 'nos', 'NO': 'nos', 'NOS': 'nos', 'PC': 'nos', 'PCS': 'nos', 'UNIT': 'nos',
  'SET': 'set', 'SETS': 'set',
  'LOT': 'lot', 'LS': 'lot', 'LUMP SUM': 'lot',
};

// ── small helpers ─────────────────────────────────────────────────────────

function boqsOf(project) { return (project && project.boqs) || []; }
function boqNum(v) { const n = Number(v); return isFinite(n) ? n : 0; }
// The one predicate that separates "nobody has priced this yet" from "this is
// genuinely free". Everything downstream — the totals, the incomplete flag, the
// printed bill — reads through it, so the two can never be collapsed by accident.
function boqRateSet(line) {
  return !(line.rate === null || line.rate === undefined || line.rate === '');
}
function boqUnpricedLines(boq) { return ((boq && boq.lines) || []).filter(l => !boqRateSet(l)); }

// Every write goes through here: it finds the bill in the draft, stamps who
// touched it and when, records it on the bill's own activity trail AND on the
// project change log. `ctx.updateProject` / `ctx.logAction` live inside App()
// and are unreachable as bare globals — calling them bare fails silently and
// the write is lost.
function boqUpdate(ctx, projectId, boqId, fn, log) {
  ctx.updateProject(projectId, draft => {
    if (!Array.isArray(draft.boqs)) draft.boqs = [];
    const b = draft.boqs.find(x => x.id === boqId);
    if (!b) return;
    fn(b, draft);
    b.modifiedBy = ctx.currentUserName || '';
    b.modifiedDate = todayISO();
    if (log) {
      if (!Array.isArray(b.activity)) b.activity = [];
      b.activity.unshift({ id: uid('boqact'), date: todayISO(), by: ctx.currentUserName || '', text: log });
      ctx.logAction(draft, `BOQ "${b.name}": ${log}`);
    }
  });
}

function boqAccountOf(ctx, project) {
  if (!project) return null;
  return (ctx.accounts || []).find(a => a.id === project.accountId) || null;
}
// The client's location decides which scopes the bill may be written against.
// familiesForRegion is a CLOSED SET rule — Abu Dhabi sees its eight and nothing
// else — so this is the only place the family list should ever come from.
function boqFamiliesFor(ctx, project) {
  return familiesForRegion(ctx.scopeLibrary, accountRegion(boqAccountOf(ctx, project)));
}

// Scopes and work items are printed in the library's own order, not in the
// order somebody happened to type the lines. A bill is read down the page and
// compared against the last revision; a shifting order makes that impossible.
function boqOrderedScopes(group, families) {
  const order = (families || []).map(f => f.name);
  return Object.keys(group.scopes).map(k => group.scopes[k]).sort((a, b) => {
    const ia = order.indexOf(a.familyName), ib = order.indexOf(b.familyName);
    return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib) || String(a.familyName).localeCompare(String(b.familyName));
  });
}
function boqOrderedItems(scopeGroup, scopeLibrary) {
  const wi = familyWorkItems(scopeGroup.familyName, scopeLibrary) || [];
  return Object.keys(scopeGroup.items).map(k => scopeGroup.items[k]).sort((a, b) => {
    const ia = wi.indexOf(a.workItem), ib = wi.indexOf(b.workItem);
    return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib) || String(a.workItem).localeCompare(String(b.workItem));
  });
}

// What the bill can honestly claim about labour. Read from the scope library's
// own supplyOnly flag rather than hardcoded to a region, so a family that is
// re-flagged changes the sentence by itself.
function boqSupplyStatus(boq, scopeLibrary) {
  const fams = [];
  ((boq && boq.lines) || []).forEach(l => { if (l.familyName && fams.indexOf(l.familyName) < 0) fams.push(l.familyName); });
  const withLabour = fams.filter(f => !familyIsSupplyOnly(f, scopeLibrary));
  return { fams, withLabour, allSupply: fams.length > 0 && withLabour.length === 0 };
}

function boqSecondaryText(boq, totals) {
  if (!totals.fxRate) return null;
  return fmtCurrency(totals.secondary, boq.secondaryCurrency || 'USD');
}
function boqFxSentence(boq) {
  if (!boq.fxRate) return 'No exchange rate has been entered, so no second-currency figure is shown. That is a blank, not a zero.';
  return `1 ${boq.currency} = ${boq.fxRate} ${boq.secondaryCurrency} — rate entered ${boq.fxDate ? fmtDate(boq.fxDate) : 'on an unrecorded date'} and held on this bill. It does not follow the market.`;
}
function boqFxAgeDays(boq) {
  if (!boq.fxRate || !boq.fxDate) return null;
  const d = daysBetween(boq.fxDate, todayISO());
  return isFinite(d) ? d : null;
}

// Move a line within its OWN group. The lines array is flat and the grouping is
// derived, so "up" means "swap with the previous line sharing the same
// apartment type, scope and work item" — swapping with the raw previous element
// would silently move the line into another group.
function boqMoveLine(lines, id, dir) {
  const i = lines.findIndex(l => l.id === id);
  if (i < 0) return lines;
  const me = lines[i];
  const same = j => lines[j].unitTypeId === me.unitTypeId && lines[j].familyName === me.familyName && lines[j].workItem === me.workItem;
  let j = dir < 0 ? i - 1 : i + 1;
  while (j >= 0 && j < lines.length && !same(j)) j += (dir < 0 ? -1 : 1);
  if (j < 0 || j >= lines.length) return lines;
  const out = lines.slice();
  out[i] = out[j]; out[j] = me;
  return out;
}

// ═══════════════════════════════════════════════════ entry point

function BoqSection({ ctx, projectId, onProject }) {
  const projects = ctx.deptProjects(ctx.projects || []).filter(p => !p.scratch);
  const all = ctx.deptProjects(ctx.projects || []);
  const project = all.find(p => p.id === projectId) || null;
  const editable = ctx.canEdit('softwares');
  const [openId, setOpenId] = useState('');

  useEffect(() => { setOpenId(''); }, [projectId]);

  // A scratch project is somebody's unassigned workspace. A bill of quantities
  // is issued to a client against a real job, so it is not offered here — but
  // being handed one must not break the screen.
  if (project && project.scratch) {
    return <BoqNeed text="This is a scratch workspace, not a job. A bill of quantities is issued to a client against a real project, so pick one of those instead." />;
  }
  if (!project) {
    return (
      <div className="space-y-4">
        <BoqNeed text="A bill of quantities belongs to a job. Pick the project it is being written for." />
        <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
          {projects.map(p => {
            const acct = boqAccountOf(ctx, p);
            return (
              <button key={p.id} onClick={() => typeof onProject === 'function' && onProject(p.id)}
                className="text-left rounded-lg border border-[var(--leon-line)] p-3 hover:border-[var(--leon-brown)]">
                <div className="font-semibold text-sm">{p.name}</div>
                <div className="text-xs text-[var(--leon-black)]/50">
                  {acct ? acct.name : 'no client'} · {accountRegionLabel(accountRegion(acct))}
                </div>
                <div className="text-[11px] text-[var(--leon-black)]/40">
                  {boqsOf(p).length} bill{boqsOf(p).length === 1 ? '' : 's'}
                </div>
              </button>
            );
          })}
          {!projects.length && <EmptyState text="No projects in this department yet." />}
        </div>
      </div>
    );
  }

  const boq = boqsOf(project).find(b => b.id === openId) || null;
  if (!boq) {
    return <BoqBillsPanel ctx={ctx} project={project} editable={editable} onOpen={setOpenId} />;
  }
  return <BoqEditor ctx={ctx} project={project} boq={boq} editable={editable} onExit={() => setOpenId('')} />;
}

function BoqNeed({ text }) {
  return (
    <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-8 text-center">
      <div className="text-3xl mb-2">🧾</div>
      <div className="text-sm text-[var(--leon-black)]/60 max-w-md mx-auto">{text}</div>
    </div>
  );
}

function BoqPanel({ title, right, children, className }) {
  return (
    <div className={`rounded-lg border border-[var(--leon-line)] bg-white ${className || ''}`}>
      <div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b border-[var(--leon-line)]">
        <div className="font-bold text-sm">{title}</div>
        <div className="flex items-center gap-2">{right}</div>
      </div>
      <div className="p-4">{children}</div>
    </div>
  );
}

function BoqStat({ label, value, sub, tone }) {
  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
      <div className="text-[11px] uppercase tracking-wide text-[var(--leon-black)]/50 font-semibold">{label}</div>
      <div className={`text-lg font-bold ${tone === 'red' ? 'text-[#b83b3b]' : ''}`}>{value}</div>
      {sub && <div className="text-[11px] text-[var(--leon-black)]/50">{sub}</div>}
    </div>
  );
}

// ═══════════════════════════════════════════════════ the bills on a job

function BoqBillsPanel({ ctx, project, editable, onOpen }) {
  const [adding, setAdding] = useState(false);
  const bills = boqsOf(project);
  const account = boqAccountOf(ctx, project);
  const families = boqFamiliesFor(ctx, project);
  const region = accountRegion(account);
  const regionAllSupply = families.length > 0 && families.every(f => !!f.supplyOnly);

  function duplicate(b) {
    const copy = cloneDeep(b);
    copy.id = uid('boq');
    copy.name = `${b.name} (copy)`;
    copy.status = 'Draft';
    copy.revision = 0;
    copy.revisions = [];
    copy.activity = [];
    copy.createdBy = ctx.currentUserName || '';
    copy.createdDate = todayISO();
    // Ids are remapped, not reused. Two records sharing an id is the exact bug
    // that once corrupted a window schedule's dependency graph.
    const map = {};
    copy.unitTypes = (copy.unitTypes || []).map(t => { const nid = uid('but'); map[t.id] = nid; return Object.assign({}, t, { id: nid }); });
    copy.lines = (copy.lines || []).map(l => Object.assign({}, l, { id: uid('boql'), unitTypeId: map[l.unitTypeId] || null }));
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.boqs)) draft.boqs = [];
      draft.boqs.push(copy);
      ctx.logAction(draft, `BOQ "${b.name}" duplicated as "${copy.name}".`);
    });
  }

  return (
    <div className="space-y-4">
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3 text-xs text-[var(--leon-black)]/70">
        <strong>{project.name}</strong>{account ? ` · ${account.name}` : ''} · {accountRegionLabel(region)}.
        {' '}This client's bills may be written against {families.length} scope{families.length === 1 ? '' : 's'}
        {families.length ? `: ${families.map(f => f.name).join(', ')}.` : '.'}
        {regionAllSupply && ' Every one of them is supply only — no labour of any kind, including countertop installation.'}
      </div>

      <div className="flex items-center justify-between gap-3">
        <h3 className="font-bold">Bills of quantities</h3>
        {editable && <Button size="sm" onClick={() => setAdding(true)}>+ New bill</Button>}
      </div>

      {!bills.length && <EmptyState text="No bill of quantities on this job yet. Start one, add the apartment types, then write the bill scope by scope." />}

      <div className="space-y-2">
        {bills.map(b => {
          const t = boqTotals(b);
          const units = (b.unitTypes || []).reduce((n, u) => n + Math.max(0, boqNum(u.count)), 0);
          return (
            <div key={b.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3 flex items-start justify-between gap-3 flex-wrap">
              <button className="text-left flex-1 min-w-[220px]" onClick={() => onOpen(b.id)}>
                <div className="font-semibold text-sm flex items-center gap-2 flex-wrap">
                  {b.name}
                  <Badge tone={b.status === 'Accepted' ? 'green' : b.status === 'Lost' ? 'red' : b.status === 'Draft' ? 'neutral' : 'yellow'}>{b.status}</Badge>
                  {b.revision > 0 && <Badge tone="blue">Rev. {b.revision}</Badge>}
                  {!t.complete && <Badge tone="red">Incomplete</Badge>}
                </div>
                <div className="text-[11px] text-[var(--leon-black)]/50">
                  {(b.unitTypes || []).length} apartment type{(b.unitTypes || []).length === 1 ? '' : 's'} · {units} apartment{units === 1 ? '' : 's'} · {(b.lines || []).length} line{(b.lines || []).length === 1 ? '' : 's'} · updated {fmtDate(b.modifiedDate)}
                </div>
              </button>
              <div className="text-right">
                {ctx.canSeeFin && (
                  <>
                    <div className="font-bold text-sm">{fmtCurrency(t.extendedAll, b.currency)}</div>
                    <div className="text-[11px] text-[var(--leon-black)]/50">
                      {t.fxRate ? boqSecondaryText(b, t) : `no ${b.secondaryCurrency} rate entered`}
                    </div>
                  </>
                )}
                {editable && <Button size="sm" variant="ghost" onClick={() => duplicate(b)}>Duplicate</Button>}
              </div>
            </div>
          );
        })}
      </div>

      <BoqNewModal open={adding} onClose={() => setAdding(false)} ctx={ctx} project={project} onCreated={onOpen} />
    </div>
  );
}

function BoqNewModal({ open, onClose, ctx, project, onCreated }) {
  const [name, setName] = useState('');
  const [currency, setCurrency] = useState('AED');
  const [secondary, setSecondary] = useState('USD');
  useEffect(() => { if (open) { setName(`${project.name} — Bill of Quantities`); setCurrency('AED'); setSecondary('USD'); } }, [open, project.id]);

  function create() {
    const rec = makeBoq({
      name: name.trim() || 'Bill of Quantities',
      projectId: project.id, accountId: project.accountId || null,
      currency, secondaryCurrency: secondary,
    }, ctx.currentUserName || '');
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.boqs)) draft.boqs = [];
      draft.boqs.push(rec);
      ctx.logAction(draft, `BOQ "${rec.name}" created (${currency}, also shown in ${secondary}).`);
    });
    onClose();
    if (typeof onCreated === 'function') onCreated(rec.id);
  }

  return (
    <Modal open={open} onClose={onClose} title="New bill of quantities" footer={<>
      <Button variant="ghost" onClick={onClose}>Cancel</Button>
      <Button onClick={create} disabled={!name.trim() || currency === secondary}>Create</Button>
    </>}>
      <div className="space-y-3">
        <Field label="Name"><TextInput value={name} onChange={e => setName(e.target.value)} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Contract currency" hint="The currency the bill is written and awarded in.">
            <Select value={currency} onChange={e => setCurrency(e.target.value)}>
              {QUOTE_CURRENCIES.map(c => <option key={c} value={c}>{currencyLabel(c)}</option>)}
            </Select>
          </Field>
          <Field label="Also shown in" hint="A conversion of the same money, at a rate you stamp — never a second typed total.">
            <Select value={secondary} onChange={e => setSecondary(e.target.value)}>
              {QUOTE_CURRENCIES.map(c => <option key={c} value={c}>{currencyLabel(c)}</option>)}
            </Select>
          </Field>
        </div>
        {currency === secondary && <div className="text-xs text-[#b83b3b]">Pick two different currencies — showing a bill twice in the same currency says nothing.</div>}
      </div>
    </Modal>
  );
}

// ═══════════════════════════════════════════════════ the editor

function BoqEditor({ ctx, project, boq, editable, onExit }) {
  const [tab, setTab] = useState('bill');
  const account = boqAccountOf(ctx, project);
  const families = boqFamiliesFor(ctx, project);
  const totals = boqTotals(boq);
  const unpriced = boqUnpricedLines(boq);
  const supply = boqSupplyStatus(boq, ctx.scopeLibrary);
  const units = (boq.unitTypes || []).reduce((n, u) => n + Math.max(0, boqNum(u.count)), 0);

  const apply = (fn, log) => boqUpdate(ctx, project.id, boq.id, fn, log);

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <button onClick={onExit} className="text-xs text-[var(--leon-brown)] font-semibold hover:underline">← All bills on this job</button>
          <h3 className="font-bold text-lg flex items-center gap-2 flex-wrap">
            {boq.name}
            <Badge tone={boq.status === 'Accepted' ? 'green' : boq.status === 'Lost' ? 'red' : boq.status === 'Draft' ? 'neutral' : 'yellow'}>{boq.status}</Badge>
            {boq.revision > 0 && <Badge tone="blue">Rev. {boq.revision}</Badge>}
          </h3>
          <div className="text-xs text-[var(--leon-black)]/60">
            {project.name}{account ? ` · ${account.name}` : ''} · {accountRegionLabel(accountRegion(account))} · {boq.currency}
          </div>
        </div>
      </div>

      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
        <BoqStat label="Apartments" value={String(units)} sub={`${(boq.unitTypes || []).length} type${(boq.unitTypes || []).length === 1 ? '' : 's'}`} />
        <BoqStat label="Lines" value={String((boq.lines || []).length)} sub={`${supply.fams.length} scope${supply.fams.length === 1 ? '' : 's'}`} />
        {ctx.canSeeFin && <BoqStat label={`Total ${boq.currency}`} value={fmtCurrency(totals.extendedAll, boq.currency)} sub={`${fmtCurrency(totals.perUnitAll, boq.currency)} per one of each type`} />}
        {ctx.canSeeFin && <BoqStat label={`Total ${boq.secondaryCurrency}`}
          value={totals.fxRate ? boqSecondaryText(boq, totals) : '—'}
          sub={totals.fxRate ? `at ${totals.fxRate}, stamped ${fmtDate(totals.fxDate)}` : 'no rate entered'} />}
      </div>

      {!totals.complete && <BoqIncompleteNote boq={boq} unpriced={unpriced} />}
      <BoqSupplyNote supply={supply} />

      <Tabs tabs={BOQ_TABS} active={tab} onChange={setTab} />

      {tab === 'types' && <BoqTypesPanel ctx={ctx} boq={boq} totals={totals} editable={editable} apply={apply} />}
      {tab === 'bill' && <BoqBillTab ctx={ctx} project={project} boq={boq} totals={totals} families={families} editable={editable} apply={apply} />}
      {tab === 'money' && <BoqMoneyPanel ctx={ctx} boq={boq} totals={totals} editable={editable} apply={apply} />}
      {tab === 'document' && (
        <BoqDocument ctx={ctx} project={project} account={account} boq={boq} totals={totals} families={families}
          actions={
            <div className="no-print flex items-center gap-1 justify-end">
              <Button size="sm" variant="outline" onClick={() => boqExportCsv(ctx, project, boq)}>Download CSV</Button>
              <Button size="sm" variant="outline" onClick={() => {
                const d = boqExportToSheet(ctx, project, boq);
                if (d) alert('Sent to LEON Sheets as "' + d.name + '". Open it under LEON Studio → LEON Sheets.');
              }}>Send to LEON Sheets</Button>
              {typeof ShareButton === 'function' && (
                <ShareButton ctx={ctx} subject={`${boq.name} — bill of quantities`} projectId={project.id}
                  subjectKey={`boq:${boq.id}`}
                  summary={`${(boq.unitTypes || []).length} apartment type(s), ${(boq.lines || []).length} lines, ${totals.complete ? fmtCurrency(totals.extendedAll, boq.currency) : 'incomplete pricing'}`} />
              )}
              <DocActions title={`${boq.name} — bill of quantities`} heading="Bill of Quantities"
                lines={[project.name, account ? account.name : '', project.address || '']} />
            </div>
          } />
      )}
      {tab === 'revisions' && <BoqRevisionsPanel ctx={ctx} project={project} account={account} boq={boq} totals={totals} families={families} editable={editable} apply={apply} />}
    </div>
  );
}

function BoqIncompleteNote({ boq, unpriced }) {
  return (
    <div className="rounded-md border border-[#e9c9c9] bg-[#fdf4f4] p-3 text-xs">
      <div className="font-bold text-[#b83b3b] mb-1">
        This bill is incomplete — {unpriced.length} line{unpriced.length === 1 ? '' : 's'} carry no rate.
      </div>
      <div className="text-[var(--leon-black)]/70 mb-1.5">
        An unset rate is not a zero. Those lines contribute nothing to the totals below, so what is shown is
        what has been priced so far and not the value of the bill.
      </div>
      <ul className="list-disc pl-4 space-y-0.5 text-[var(--leon-black)]/70">
        {unpriced.slice(0, 12).map(l => {
          const t = (boq.unitTypes || []).find(u => u.id === l.unitTypeId);
          return <li key={l.id}>{t ? (t.code || t.name) : 'no apartment type'} · {l.familyName || 'no scope'} · {l.workItem || '—'} · {l.description || 'no description'}</li>;
        })}
        {unpriced.length > 12 && <li>…and {unpriced.length - 12} more.</li>}
      </ul>
    </div>
  );
}

function BoqSupplyNote({ supply }) {
  if (!supply.fams.length) return null;
  if (supply.allSupply) {
    return (
      <div className="rounded-md border border-[#cddbc9] bg-[#f3f8f3] p-3 text-xs text-[var(--leon-black)]/75">
        <strong>Supply only.</strong> Every scope on this bill is flagged supply-only in the scope library:
        materials are supplied and delivered, and no installation, fixing, site labour or making good is
        included or priced — countertops included.
      </div>
    );
  }
  return (
    <div className="rounded-md border border-[#eee0c4] bg-[#fdfaf2] p-3 text-xs text-[var(--leon-black)]/75">
      <strong>Mostly supply only.</strong> These scopes are <em>not</em> flagged supply-only in the scope
      library: {supply.withLabour.join(', ')}. Say what is included for them before this bill goes out — the
      printed page will not claim supply-only for the whole bill while they are on it.
    </div>
  );
}

// ═══════════════════════════════════════════════════ apartment types

function BoqTypesPanel({ ctx, boq, totals, editable, apply }) {
  const types = boq.unitTypes || [];
  const used = id => (boq.lines || []).filter(l => l.unitTypeId === id).length;

  function add() {
    apply(b => { b.unitTypes = (b.unitTypes || []).concat([makeBoqUnitType({ code: '', name: '', count: 1 })]); },
      'apartment type added.');
  }
  function set(id, field, value) {
    apply(b => { const t = (b.unitTypes || []).find(x => x.id === id); if (t) t[field] = value; });
  }
  function remove(t) {
    apply(b => { b.unitTypes = (b.unitTypes || []).filter(x => x.id !== t.id); },
      `apartment type ${t.code || t.name || '(unnamed)'} removed.`);
  }

  return (
    <div className="space-y-3">
      <div className="rounded-md border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3 text-xs text-[var(--leon-black)]/75">
        The count is the multiplier. Every quantity on the bill is entered for <strong>one</strong> apartment of
        its type and then multiplied by the number here, which is why nobody measures thirty identical
        apartments. Change a count and every subtotal on the bill moves with it.
      </div>

      <BoqPanel title="Apartment types" right={editable && <Button size="sm" onClick={add}>+ Add type</Button>}>
        {!types.length && <EmptyState text="No apartment types yet. Add one for each unit category on the tower — Type A, Type B, the penthouse." />}
        {types.length > 0 && (
          <div className="overflow-x-auto">
            <table className="w-full text-sm">
              <thead>
                <tr className="text-left text-[11px] uppercase tracking-wide text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
                  <th className="py-1.5 pr-2">Code</th>
                  <th className="py-1.5 pr-2">Name</th>
                  <th className="py-1.5 pr-2 text-right">Apartments</th>
                  <th className="py-1.5 pr-2 text-right">Area m² each</th>
                  <th className="py-1.5 pr-2">Notes</th>
                  <th className="py-1.5 pr-2 text-right">Lines</th>
                  {ctx.canSeeFin && <th className="py-1.5 pr-2 text-right">Per apartment</th>}
                  {ctx.canSeeFin && <th className="py-1.5 pr-2 text-right">Extended</th>}
                  <th className="py-1.5" />
                </tr>
              </thead>
              <tbody>
                {types.map(t => {
                  const g = (totals.groups || []).find(x => x.type && x.type.id === t.id);
                  const n = used(t.id);
                  return (
                    <tr key={t.id} className="border-b border-[var(--leon-line)] align-top">
                      <td className="py-1.5 pr-2 w-20">
                        <TextInput value={t.code || ''} disabled={!editable} placeholder="A"
                          onChange={e => set(t.id, 'code', e.target.value)} className="!py-1 !text-sm" />
                      </td>
                      <td className="py-1.5 pr-2">
                        <TextInput value={t.name || ''} disabled={!editable} placeholder="1-Bed Type A"
                          onChange={e => set(t.id, 'name', e.target.value)} className="!py-1 !text-sm" />
                      </td>
                      <td className="py-1.5 pr-2 w-28">
                        <TextInput type="number" min="0" step="1" value={t.count === null || t.count === undefined ? '' : t.count}
                          disabled={!editable} onChange={e => set(t.id, 'count', e.target.value === '' ? 0 : Number(e.target.value))}
                          className="!py-1 !text-sm text-right" />
                      </td>
                      <td className="py-1.5 pr-2 w-28">
                        <TextInput type="number" min="0" step="any" value={t.areaSqm === null || t.areaSqm === undefined ? '' : t.areaSqm}
                          disabled={!editable} onChange={e => set(t.id, 'areaSqm', e.target.value === '' ? null : Number(e.target.value))}
                          className="!py-1 !text-sm text-right" placeholder="—" />
                      </td>
                      <td className="py-1.5 pr-2">
                        <TextInput value={t.notes || ''} disabled={!editable}
                          onChange={e => set(t.id, 'notes', e.target.value)} className="!py-1 !text-sm" />
                      </td>
                      <td className="py-1.5 pr-2 text-right">{n}</td>
                      {ctx.canSeeFin && <td className="py-1.5 pr-2 text-right whitespace-nowrap">{g ? fmtCurrency(g.perUnit, boq.currency) : '—'}</td>}
                      {ctx.canSeeFin && (
                        <td className="py-1.5 pr-2 text-right whitespace-nowrap font-semibold">
                          {g ? fmtCurrency(g.extended, boq.currency) : '—'}
                          {g && <div className="text-[10px] font-normal text-[var(--leon-black)]/45">× {Math.max(0, boqNum(t.count))}</div>}
                        </td>
                      )}
                      <td className="py-1.5 text-right">
                        {editable && (
                          n > 0
                            ? <span className="text-[10px] text-[var(--leon-black)]/40" title="Remove the lines first — deleting a type that lines point at would strand them.">in use</span>
                            : <IconAction icon="✕" title="Remove this apartment type" onClick={() => remove(t)} />
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </BoqPanel>
    </div>
  );
}

// ═══════════════════════════════════════════════════ the bill

function BoqBillTab({ ctx, project, boq, totals, families, editable, apply }) {
  const [adding, setAdding] = useState(false);
  const [importing, setImporting] = useState(false);
  const [focusId, setFocusId] = useState('');
  const types = boq.unitTypes || [];

  // Focus lands on the row that was just created. Doing it by attribute rather
  // than a ref map means a row added inside any group is reachable without the
  // parent having to hold a ref for every line on the bill.
  useEffect(() => {
    if (!focusId) return;
    const el = document.querySelector(`[data-boq-line="${focusId}"]`);
    if (el && typeof el.focus === 'function') el.focus();
    setFocusId('');
  }, [focusId]);

  function addLine(seed, afterId) {
    const rec = makeBoqLine(seed || {});
    apply(b => {
      b.lines = b.lines || [];
      const i = afterId ? b.lines.findIndex(l => l.id === afterId) : -1;
      if (i >= 0) b.lines.splice(i + 1, 0, rec); else b.lines.push(rec);
    }, afterId ? null : `line added under ${seed && seed.workItem ? seed.workItem : 'the bill'}.`);
    setFocusId(rec.id);
    return rec.id;
  }
  function setLine(id, field, value) {
    apply(b => { const l = (b.lines || []).find(x => x.id === id); if (l) l[field] = value; });
  }
  function dupLine(l) {
    // A fresh id, always. Two records sharing one id is the bug that once
    // corrupted a window schedule's dependency graph.
    const seed = cloneDeep(l);
    delete seed.id;
    const rec = makeBoqLine(seed);
    apply(b => {
      const i = (b.lines || []).findIndex(x => x.id === l.id);
      if (i >= 0) b.lines.splice(i + 1, 0, rec); else b.lines.push(rec);
    }, `line "${l.description || l.workItem}" duplicated.`);
    setFocusId(rec.id);
  }
  function removeLine(l) {
    apply(b => { b.lines = (b.lines || []).filter(x => x.id !== l.id); },
      `line "${l.description || l.workItem || '(blank)'}" removed.`);
  }
  function move(id, dir) {
    apply(b => { b.lines = boqMoveLine(b.lines || [], id, dir); });
  }

  if (!types.length) {
    return <BoqNeed text="Add the apartment types first. Every line of this bill is a quantity for one apartment of a type, so the bill cannot be written until the types exist." />;
  }

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="text-xs text-[var(--leon-black)]/60">
          Quantities are <strong>per apartment</strong>. Tab moves to the next field, Enter starts a new line in
          the same group, ⧉ copies a line and ↑ ↓ reorder within the group.
        </div>
        {editable && (
          <div className="flex items-center gap-2">
            <Button size="sm" variant="outline" onClick={() => setImporting(true)}>Import from take-off</Button>
            <Button size="sm" onClick={() => setAdding(true)}>+ Add line</Button>
          </div>
        )}
      </div>

      {(totals.groups || []).map(g => (
        <BoqTypeGroup key={g.type ? g.type.id : 'orphan'} ctx={ctx} boq={boq} group={g} families={families}
          editable={editable} onAdd={addLine} onSet={setLine} onDup={dupLine} onRemove={removeLine} onMove={move} />
      ))}

      {!(boq.lines || []).length && <EmptyState text="No lines yet. Add one, or import the measurements already taken off on this job." />}

      {ctx.canSeeFin && (boq.lines || []).length > 0 && (
        <div className="rounded-lg border-2 border-[var(--leon-black)] bg-white p-4">
          <div className="flex justify-between text-sm">
            <span className="text-[var(--leon-black)]/60">One of each apartment type</span>
            <span>{fmtCurrency(totals.perUnitAll, boq.currency)}</span>
          </div>
          <div className="flex justify-between font-bold text-base mt-1">
            <span>{totals.complete ? `Bill total — ${boq.currency}` : `Priced so far — ${boq.currency}`}</span>
            <span>{fmtCurrency(totals.extendedAll, boq.currency)}</span>
          </div>
          <div className="flex justify-between text-sm mt-0.5">
            <span className="text-[var(--leon-black)]/60">{boq.secondaryCurrency}</span>
            <span>{totals.fxRate ? boqSecondaryText(boq, totals) : <span className="text-[#b83b3b] text-xs">no rate entered</span>}</span>
          </div>
          <div className="text-[11px] text-[var(--leon-black)]/50 mt-1">{boqFxSentence(boq)}</div>
        </div>
      )}

      <BoqAddLineModal open={adding} onClose={() => setAdding(false)} boq={boq} families={families}
        scopeLibrary={ctx.scopeLibrary} onAdd={seed => { addLine(seed); setAdding(false); }} />
      <BoqImportModal open={importing} onClose={() => setImporting(false)} ctx={ctx} project={project}
        boq={boq} families={families} apply={apply} />
    </div>
  );
}

function BoqTypeGroup({ ctx, boq, group, families, editable, onAdd, onSet, onDup, onRemove, onMove }) {
  const t = group.type;
  const count = t ? Math.max(0, boqNum(t.count)) : 1;
  const scopes = boqOrderedScopes(group, families);

  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
      <div className="px-4 py-2.5 bg-[var(--leon-cream)] border-b border-[var(--leon-line)] flex items-center justify-between gap-3 flex-wrap">
        <div className="font-bold text-sm">
          {t ? `${t.code ? t.code + ' — ' : ''}${t.name || 'Unnamed type'}` : 'Unassigned'}
          <span className="ml-2 font-normal text-[var(--leon-black)]/60">
            {t ? `× ${count} apartment${count === 1 ? '' : 's'}` : 'these lines point at an apartment type that no longer exists'}
          </span>
        </div>
        {ctx.canSeeFin && (
          <div className="text-xs text-right">
            <div className="text-[var(--leon-black)]/55">{fmtCurrency(group.perUnit, boq.currency)} per apartment × {count}</div>
            <div className="font-bold text-sm">{fmtCurrency(group.extended, boq.currency)}</div>
          </div>
        )}
      </div>

      <div className="p-3 space-y-3">
        {scopes.map(sc => (
          <div key={sc.familyName} className="rounded-md border border-[var(--leon-line)]">
            <div className="px-3 py-2 flex items-center justify-between gap-3 flex-wrap border-b border-[var(--leon-line)]">
              <div className="font-semibold text-sm flex items-center gap-2 flex-wrap">
                {sc.familyName}
                {familyIsSupplyOnly(sc.familyName, ctx.scopeLibrary)
                  ? <Badge tone="green">Supply only</Badge>
                  : <Badge tone="yellow">Not flagged supply-only</Badge>}
                {sc.unpriced > 0 && <Badge tone="red">{sc.unpriced} unpriced</Badge>}
              </div>
              {ctx.canSeeFin && (
                <div className="text-xs text-right">
                  <span className="text-[var(--leon-black)]/55">{fmtCurrency(sc.perUnit, boq.currency)} × {count} = </span>
                  <span className="font-bold">{fmtCurrency(sc.extended, boq.currency)}</span>
                </div>
              )}
            </div>
            <div className="p-2 space-y-2">
              {boqOrderedItems(sc, ctx.scopeLibrary).map(it => (
                <div key={it.workItem}>
                  <div className="flex items-center justify-between gap-2 px-1 py-1">
                    <div className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/60">{it.workItem}</div>
                    <div className="flex items-center gap-2">
                      {ctx.canSeeFin && <span className="text-xs text-[var(--leon-black)]/55">{fmtCurrency(it.extended, boq.currency)}</span>}
                      {editable && (
                        <IconAction icon="+" title={`Add a line under ${it.workItem}`}
                          onClick={() => onAdd({ unitTypeId: t ? t.id : null, familyName: sc.familyName, workItem: it.workItem })} />
                      )}
                    </div>
                  </div>
                  <div className="overflow-x-auto">
                    <table className="w-full text-sm">
                      <thead>
                        <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
                          <th className="py-1 pr-2">Description</th>
                          <th className="py-1 pr-2">Specification</th>
                          <th className="py-1 pr-2 text-right">Qty / apt</th>
                          <th className="py-1 pr-2">Unit</th>
                          {ctx.canSeeFin && <th className="py-1 pr-2 text-right">Rate</th>}
                          {ctx.canSeeFin && <th className="py-1 pr-2 text-right">Per apt</th>}
                          {ctx.canSeeFin && <th className="py-1 pr-2 text-right">× {count}</th>}
                          <th className="py-1" />
                        </tr>
                      </thead>
                      <tbody>
                        {it.lines.map(l => (
                          <BoqLineRow key={l.id} ctx={ctx} boq={boq} line={l} count={count} editable={editable}
                            onSet={onSet} onDup={onDup} onRemove={onRemove} onMove={onMove}
                            onEnter={() => onAdd({ unitTypeId: l.unitTypeId, familyName: l.familyName, workItem: l.workItem, unit: l.unit }, l.id)} />
                        ))}
                      </tbody>
                    </table>
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}
        {!scopes.length && <EmptyState text="No lines under this apartment type yet." />}
      </div>
    </div>
  );
}

function BoqLineRow({ ctx, boq, line, count, editable, onSet, onDup, onRemove, onMove, onEnter }) {
  const rateSet = boqRateSet(line);
  const per = rateSet ? boqNum(line.qty) * boqNum(line.rate) : 0;
  const key = e => { if (e.key === 'Enter') { e.preventDefault(); onEnter(); } };
  return (
    <tr className="border-b border-[var(--leon-line)] align-top">
      <td className="py-1 pr-2 min-w-[180px]">
        <TextInput value={line.description || ''} disabled={!editable} data-boq-line={line.id}
          placeholder="What is being supplied" onKeyDown={key}
          onChange={e => onSet(line.id, 'description', e.target.value)} className="!py-1 !text-sm" />
        <TextInput value={line.notes || ''} disabled={!editable} placeholder="Notes (optional)" onKeyDown={key}
          onChange={e => onSet(line.id, 'notes', e.target.value)} className="!py-1 !text-[11px] mt-1" />
      </td>
      <td className="py-1 pr-2 min-w-[150px]">
        <TextInput value={line.spec || ''} disabled={!editable} placeholder="Make, model, finish" onKeyDown={key}
          onChange={e => onSet(line.id, 'spec', e.target.value)} className="!py-1 !text-sm" />
      </td>
      <td className="py-1 pr-2 w-24">
        <TextInput type="number" step="any" min="0" value={line.qty === null || line.qty === undefined ? '' : line.qty}
          disabled={!editable} onKeyDown={key}
          onChange={e => onSet(line.id, 'qty', e.target.value === '' ? 0 : Number(e.target.value))}
          className="!py-1 !text-sm text-right" />
      </td>
      <td className="py-1 pr-2 w-24">
        <Select value={line.unit || 'nos'} disabled={!editable} onKeyDown={key}
          onChange={e => onSet(line.id, 'unit', e.target.value)} className="!py-1 !text-sm">
          {BOQ_UNITS.map(u => <option key={u} value={u}>{u}</option>)}
        </Select>
      </td>
      {ctx.canSeeFin && (
        <td className="py-1 pr-2 w-28">
          {/* Blank means nobody has priced this. It is NOT zero, and clearing
              the field puts it back to blank rather than to 0. */}
          <TextInput type="number" step="any" min="0" value={rateSet ? line.rate : ''}
            disabled={!editable} placeholder="not priced" onKeyDown={key}
            onChange={e => onSet(line.id, 'rate', e.target.value === '' ? null : Number(e.target.value))}
            className={`!py-1 !text-sm text-right ${rateSet ? '' : 'border-[#e0b4b4]'}`} />
        </td>
      )}
      {ctx.canSeeFin && (
        <td className="py-1 pr-2 text-right whitespace-nowrap">
          {rateSet ? fmtCurrency(per, boq.currency) : <span className="text-[#b83b3b] text-xs font-semibold">not priced</span>}
        </td>
      )}
      {ctx.canSeeFin && (
        <td className="py-1 pr-2 text-right whitespace-nowrap font-semibold">
          {rateSet ? fmtCurrency(per * count, boq.currency) : '—'}
        </td>
      )}
      <td className="py-1 text-right whitespace-nowrap">
        {editable && (
          <span className="inline-flex gap-0.5">
            <IconAction icon="↑" title="Move up within this work item" onClick={() => onMove(line.id, -1)} />
            <IconAction icon="↓" title="Move down within this work item" onClick={() => onMove(line.id, 1)} />
            <IconAction icon="⧉" title="Duplicate this line" onClick={() => onDup(line)} />
            <IconAction icon="✕" title="Remove this line" onClick={() => onRemove(line)} />
          </span>
        )}
      </td>
    </tr>
  );
}

function BoqAddLineModal({ open, onClose, boq, families, scopeLibrary, onAdd }) {
  const [unitTypeId, setUnitTypeId] = useState('');
  const [familyName, setFamilyName] = useState('');
  const [workItem, setWorkItem] = useState('');
  const [description, setDescription] = useState('');
  const [unit, setUnit] = useState('nos');

  useEffect(() => {
    if (!open) return;
    const t = (boq.unitTypes || [])[0];
    setUnitTypeId(t ? t.id : '');
    setFamilyName(''); setWorkItem(''); setDescription(''); setUnit('nos');
  }, [open]);

  const items = familyWorkItems(familyName, scopeLibrary) || [];

  return (
    <Modal open={open} onClose={onClose} title="Add a line" footer={<>
      <Button variant="ghost" onClick={onClose}>Cancel</Button>
      <Button disabled={!unitTypeId || !familyName || !workItem}
        onClick={() => onAdd({ unitTypeId, familyName, workItem, description: description.trim(), unit })}>Add</Button>
    </>}>
      <div className="space-y-3">
        <Field label="Apartment type" hint="The quantity you enter is for one apartment of this type.">
          <Select value={unitTypeId} onChange={e => setUnitTypeId(e.target.value)}>
            <option value="">— choose —</option>
            {(boq.unitTypes || []).map(t => (
              <option key={t.id} value={t.id}>{t.code ? `${t.code} — ` : ''}{t.name || 'Unnamed'} (× {Math.max(0, boqNum(t.count))})</option>
            ))}
          </Select>
        </Field>
        <Field label="Scope" hint="Only the scopes this client's region sells are offered.">
          <Select value={familyName} onChange={e => { setFamilyName(e.target.value); setWorkItem(''); }}>
            <option value="">— choose —</option>
            {families.map(f => <option key={f.name} value={f.name}>{f.name}{f.supplyOnly ? ' (supply only)' : ''}</option>)}
          </Select>
        </Field>
        <Field label="Work item" hint="The BOQ groupings inside that scope.">
          <Select value={workItem} onChange={e => setWorkItem(e.target.value)} disabled={!familyName}>
            <option value="">{familyName ? '— choose —' : 'pick a scope first'}</option>
            {items.map(w => <option key={w} value={w}>{w}</option>)}
          </Select>
          {familyName && !items.length && (
            <span className="block text-[11px] text-[#b83b3b] mt-1">
              This scope has no work items defined in the scope library, so there is nothing to file a line
              under. Add them under LEON Collection first.
            </span>
          )}
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Description"><TextInput value={description} onChange={e => setDescription(e.target.value)} /></Field>
          <Field label="Unit">
            <Select value={unit} onChange={e => setUnit(e.target.value)}>
              {BOQ_UNITS.map(u => <option key={u} value={u}>{u}</option>)}
            </Select>
          </Field>
        </div>
      </div>
    </Modal>
  );
}

// ═══════════════════════════════════════════════════ currency, FX, terms

function BoqMoneyPanel({ ctx, boq, totals, editable, apply }) {
  const [rate, setRate] = useState('');
  useEffect(() => { setRate(boq.fxRate === null || boq.fxRate === undefined ? '' : String(boq.fxRate)); }, [boq.id, boq.fxRate]);
  const canMoney = editable && ctx.canSeeFin;
  const age = boqFxAgeDays(boq);
  const dirty = String(rate) !== String(boq.fxRate === null || boq.fxRate === undefined ? '' : boq.fxRate);

  // Stamping is a deliberate act with its own button because the DATE is the
  // point. Saving the rate on every keystroke would re-stamp it continuously
  // and the stamp would stop meaning "this is the rate we agreed on that day".
  function stamp() {
    const n = rate === '' ? null : Number(rate);
    apply(b => {
      b.fxRate = n === null || !isFinite(n) || n <= 0 ? null : n;
      b.fxDate = b.fxRate ? todayISO() : null;
    }, n ? `exchange rate stamped: 1 ${boq.currency} = ${n} ${boq.secondaryCurrency}.` : 'exchange rate cleared.');
  }

  return (
    <div className="space-y-3">
      <BoqPanel title="Currencies">
        <div className="grid gap-3 sm:grid-cols-2">
          <Field label="Contract currency" hint="What the bill is written and awarded in.">
            <Select value={boq.currency} disabled={!canMoney}
              onChange={e => { const v = e.target.value; apply(b => { b.currency = v; }, `contract currency set to ${v}.`); }}>
              {QUOTE_CURRENCIES.map(c => <option key={c} value={c}>{currencyLabel(c)}</option>)}
            </Select>
          </Field>
          <Field label="Also shown in" hint="A conversion of the same money — never an independently typed total.">
            <Select value={boq.secondaryCurrency} disabled={!canMoney}
              onChange={e => { const v = e.target.value; apply(b => { b.secondaryCurrency = v; }, `second currency set to ${v}.`); }}>
              {QUOTE_CURRENCIES.map(c => <option key={c} value={c}>{currencyLabel(c)}</option>)}
            </Select>
          </Field>
        </div>
      </BoqPanel>

      <BoqPanel title="Exchange rate">
        <div className="text-xs text-[var(--leon-black)]/70 mb-3">
          The rate is entered once and held on this bill with the date it was entered. It is deliberately
          <strong> not live</strong>: a rate that moves on its own would silently change a total that has
          already been sent to the client, which is the one thing a quotation must never do. Re-stamp it when
          you decide to, and the new date is recorded.
        </div>
        <div className="flex items-end gap-3 flex-wrap">
          <Field label={`${boq.secondaryCurrency} per 1 ${boq.currency}`} className="w-52">
            <TextInput type="number" step="any" min="0" value={rate} disabled={!canMoney}
              placeholder="e.g. 0.2723" onChange={e => setRate(e.target.value)} />
          </Field>
          {canMoney && <Button size="sm" onClick={stamp} disabled={!dirty}>{rate === '' ? 'Clear the rate' : 'Stamp this rate'}</Button>}
        </div>
        <div className="text-xs text-[var(--leon-black)]/60 mt-2">{boqFxSentence(boq)}</div>
        {age !== null && age > 30 && (
          <div className="text-xs text-[#a67b1f] mt-1">
            That rate was stamped {age} days ago. Check it before this bill goes out again.
          </div>
        )}
        {ctx.canSeeFin && (
          <div className="mt-3 rounded-md bg-[var(--leon-cream)] p-3 text-sm">
            <div className="flex justify-between"><span className="text-[var(--leon-black)]/60">Bill total</span><span className="font-bold">{fmtCurrency(totals.extendedAll, boq.currency)}</span></div>
            <div className="flex justify-between">
              <span className="text-[var(--leon-black)]/60">Converted</span>
              <span className="font-bold">{totals.fxRate ? boqSecondaryText(boq, totals) : <span className="text-[#b83b3b] text-xs">no rate entered</span>}</span>
            </div>
          </div>
        )}
      </BoqPanel>

      <BoqPanel title="Status, notes and terms">
        <div className="space-y-3">
          <div className="grid gap-3 sm:grid-cols-2">
            <Field label="Name"><TextInput value={boq.name} disabled={!editable}
              onChange={e => apply(b => { b.name = e.target.value; })} /></Field>
            <Field label="Status">
              <Select value={boq.status} disabled={!editable}
                onChange={e => { const v = e.target.value; apply(b => { b.status = v; }, `status set to ${v}.`); }}>
                {BOQ_STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
              </Select>
            </Field>
          </div>
          <Field label="Notes" hint="Printed under the bill.">
            <TextArea rows={3} value={boq.notes || ''} disabled={!editable}
              onChange={e => apply(b => { b.notes = e.target.value; })} />
          </Field>
          <Field label="Terms" hint="Payment, validity, delivery — printed under the bill.">
            <TextArea rows={3} value={boq.terms || ''} disabled={!editable}
              onChange={e => apply(b => { b.terms = e.target.value; })} />
          </Field>
        </div>
      </BoqPanel>
    </div>
  );
}

// ═══════════════════════════════════════════════════ the document

function boqFlatRows(boq, scopeLibrary) {
  const types = boq.unitTypes || [];
  return (boq.lines || []).map(l => {
    const t = types.find(x => x.id === l.unitTypeId) || null;
    const count = t ? Math.max(0, boqNum(t.count)) : 0;
    const set = boqRateSet(l);
    const per = set ? boqNum(l.qty) * boqNum(l.rate) : null;
    return {
      typeCode: t ? (t.code || '') : '',
      typeName: t ? (t.name || '') : 'Unassigned',
      apartments: count,
      scope: l.familyName || '',
      supplyOnly: l.familyName ? (familyIsSupplyOnly(l.familyName, scopeLibrary) ? 'Yes' : 'No') : '',
      workItem: l.workItem || '',
      description: l.description || '',
      spec: l.spec || '',
      qty: boqNum(l.qty),
      unit: l.unit || '',
      rate: set ? boqNum(l.rate) : 'NOT PRICED',
      perApartment: per === null ? 'NOT PRICED' : per,
      extended: per === null ? 'NOT PRICED' : per * count,
      currency: boq.currency,
      notes: l.notes || '',
    };
  });
}

// One column definition, read by both exports — a CSV and a sheet that
// disagreed about their columns would be two different bills.
function boqCsvColumns() {
  return [
    { key: 'typeCode', label: 'Apartment type code' },
    { key: 'typeName', label: 'Apartment type' },
    { key: 'apartments', label: 'Apartments of this type' },
    { key: 'scope', label: 'Scope' },
    { key: 'supplyOnly', label: 'Supply only' },
    { key: 'workItem', label: 'Work item' },
    { key: 'description', label: 'Description' },
    { key: 'spec', label: 'Specification' },
    { key: 'qty', label: 'Qty per apartment' },
    { key: 'unit', label: 'Unit' },
    { key: 'rate', label: 'Rate' },
    { key: 'perApartment', label: 'Amount per apartment' },
    { key: 'extended', label: 'Extended amount' },
    { key: 'currency', label: 'Currency' },
    { key: 'notes', label: 'Notes' },
  ];
}

function boqExportCsv(ctx, project, boq) {
  const cols = boqCsvColumns();
  const name = `${project.name} - ${boq.name}${boq.revision ? ` Rev ${boq.revision}` : ''}`.replace(/[^\w\- ]+/g, '');
  downloadCsv(name, cols, boqFlatRows(boq, ctx.scopeLibrary));
}

// The same flat rows, but into LEON Sheets as a real document rather than a file
// on someone's desktop. A bill that lands in Sheets stays on the job, can be
// shared and printed with everything else, and can be worked on further —
// which a CSV in Downloads cannot. Available on every bill regardless of the
// client's location; the currency travels with the numbers.
function boqExportToSheet(ctx, project, boq) {
  if (typeof ctx.exportToSheet !== 'function') return null;
  const cols = boqCsvColumns();
  const flat = boqFlatRows(boq, ctx.scopeLibrary);
  const header = cols.map(c => ({ v: c.label, bold: true }));
  const money = '#,##0.00';
  const rows = [header].concat(flat.map(r => cols.map(c => {
    const v = r[c.key];
    if (v === null || v === undefined || v === '') return '';
    const isMoney = c.key === 'rate' || c.key === 'perApartment' || c.key === 'extended';
    return isMoney ? { v: Number(v), fmt: money } : v;
  })));
  // A total row, so the sheet answers the same question the document does.
  const t = boqTotals(boq);
  const extIdx = cols.findIndex(c => c.key === 'extended');
  const totalRow = cols.map((c, i) => {
    if (i === 0) return { v: t.complete ? 'TOTAL' : 'PRICED SO FAR', bold: true };
    if (i === extIdx) return { v: t.extendedAll, bold: true, fmt: money };
    return '';
  });
  rows.push([]);
  rows.push(totalRow);
  return ctx.exportToSheet({
    name: `${project.name} — ${boq.name}${boq.revision ? ' Rev ' + boq.revision : ''}`,
    sheetName: 'Bill of Quantities',
    projectId: project.id,
    source: 'a bill of quantities',
    rows,
    colWidths: [10, 22, 14, 20, 34, 24, 12, 8, 12, 16, 16, 10, 24],
  });
}

// The issued page. It reads as a bill and not as a screen: who it is for, what
// revision it is, the three levels with a subtotal at each, the grand total in
// both currencies with the rate and its date printed on the page, and the
// supply-only statement in words.
function BoqDocument({ ctx, project, account, boq, totals, families, actions }) {
  const supply = boqSupplyStatus(boq, ctx.scopeLibrary);
  const unpriced = boqUnpricedLines(boq);
  const showMoney = ctx.canSeeFin;

  return (
    <div data-print-region className="space-y-4 bg-white rounded-lg border border-[var(--leon-line)] p-4">
      {actions}

      <div className="text-sm">
        <div className="lp-section-title font-bold text-base">Bill of Quantities</div>
        <div className="font-semibold">{boq.name}</div>
        <div className="text-[var(--leon-black)]/60">
          {project.name}{project.projectNumber ? ` · ${project.projectNumber}` : ''}{account ? ` · ${account.name}` : ''}
          {project.address ? ` · ${project.address}` : ''}
        </div>
        <div className="text-[11px] text-[var(--leon-black)]/55">
          {accountRegionLabel(accountRegion(account))} · Revision {boq.revision || 0} · {boq.status} · dated {fmtDate(boq.modifiedDate || todayISO())} ·
          {' '}priced in {currencyLabel(boq.currency)}
        </div>
      </div>

      <div className="text-xs border border-[var(--leon-line)] rounded-md p-2.5">
        {supply.allSupply
          ? <span><strong>SUPPLY ONLY.</strong> This bill covers materials supplied and delivered. No installation,
              fixing, site labour or making good is included or priced — countertops included.</span>
          : supply.withLabour.length
            ? <span><strong>Supply only, except:</strong> {supply.withLabour.join(', ')}. Those scopes are not flagged
                supply-only in the scope library, so what is included for them must be stated in the terms below.</span>
            : <span>Nothing has been billed yet, so there is nothing to state about what is included.</span>}
      </div>

      <div className="text-xs border border-[var(--leon-line)] rounded-md p-2.5">
        <strong>How to read this bill.</strong> Every quantity is for <strong>one apartment</strong> of its type.
        The extended amount is that quantity × the rate × the number of apartments of that type, and both figures
        are printed so the arithmetic can be checked line by line.
      </div>

      {(totals.groups || []).map(g => {
        const t = g.type;
        const count = t ? Math.max(0, boqNum(t.count)) : 1;
        return (
          <div key={t ? t.id : 'orphan'} className="space-y-2">
            <div className="lp-section-title font-bold text-sm border-b border-[var(--leon-black)] pb-1">
              {t ? `${t.code ? t.code + ' — ' : ''}${t.name || 'Unnamed type'} · ${count} apartment${count === 1 ? '' : 's'}` : 'Unassigned lines'}
              {t && t.areaSqm ? ` · ${t.areaSqm} m² each` : ''}
            </div>
            {boqOrderedScopes(g, families).map(sc => (
              <div key={sc.familyName} className="space-y-1">
                <div className="font-semibold text-sm mt-2">
                  {sc.familyName}
                  {familyIsSupplyOnly(sc.familyName, ctx.scopeLibrary) ? ' — supply only' : ''}
                </div>
                <table className="w-full text-sm">
                  <thead>
                    <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
                      <th className="py-1 pr-2">Item</th>
                      <th className="py-1 pr-2 text-right">Qty / apt</th>
                      <th className="py-1 pr-2">Unit</th>
                      {showMoney && <th className="py-1 pr-2 text-right">Rate</th>}
                      {showMoney && <th className="py-1 pr-2 text-right">Per apt</th>}
                      {showMoney && <th className="py-1 pr-2 text-right">× {count}</th>}
                    </tr>
                  </thead>
                  <tbody>
                    {boqOrderedItems(sc, ctx.scopeLibrary).map(it => (
                      <React.Fragment key={it.workItem}>
                        <tr className="border-t border-[var(--leon-line)]">
                          <td colSpan={showMoney ? 6 : 3} className="py-1 font-semibold text-xs uppercase tracking-wide text-[var(--leon-black)]/60">
                            {it.workItem}
                          </td>
                        </tr>
                        {it.lines.map(l => {
                          const set = boqRateSet(l);
                          const per = set ? boqNum(l.qty) * boqNum(l.rate) : null;
                          return (
                            <tr key={l.id} className="border-t border-[var(--leon-line)] align-top">
                              <td className="py-1 pr-2">
                                <div>{l.description || '—'}</div>
                                {l.spec && <div className="text-[11px] text-[var(--leon-black)]/55">{l.spec}</div>}
                                {l.notes && <div className="text-[11px] text-[var(--leon-black)]/45">{l.notes}</div>}
                              </td>
                              <td className="py-1 pr-2 text-right whitespace-nowrap">{boqNum(l.qty)}</td>
                              <td className="py-1 pr-2 whitespace-nowrap">{l.unit}</td>
                              {showMoney && <td className="py-1 pr-2 text-right whitespace-nowrap">
                                {set ? fmtCurrency(l.rate, boq.currency) : <span className="font-semibold text-[#b83b3b]">-No rate-</span>}
                              </td>}
                              {showMoney && <td className="py-1 pr-2 text-right whitespace-nowrap">{per === null ? '—' : fmtCurrency(per, boq.currency)}</td>}
                              {showMoney && <td className="py-1 pr-2 text-right whitespace-nowrap">{per === null ? '—' : fmtCurrency(per * count, boq.currency)}</td>}
                            </tr>
                          );
                        })}
                        {showMoney && (
                          <tr className="border-t border-[var(--leon-line)]">
                            <td colSpan={4} className="py-1 text-right text-[11px] text-[var(--leon-black)]/60">{it.workItem} subtotal</td>
                            <td className="py-1 pr-2 text-right whitespace-nowrap">{fmtCurrency(it.perUnit, boq.currency)}</td>
                            <td className="py-1 pr-2 text-right whitespace-nowrap font-semibold">{fmtCurrency(it.extended, boq.currency)}</td>
                          </tr>
                        )}
                      </React.Fragment>
                    ))}
                    {showMoney && (
                      <tr className="border-t-2 border-[var(--leon-black)]">
                        <td colSpan={4} className="py-1 text-right text-xs font-semibold">{sc.familyName} subtotal</td>
                        <td className="py-1 pr-2 text-right whitespace-nowrap font-semibold">{fmtCurrency(sc.perUnit, boq.currency)}</td>
                        <td className="py-1 pr-2 text-right whitespace-nowrap font-bold">{fmtCurrency(sc.extended, boq.currency)}</td>
                      </tr>
                    )}
                  </tbody>
                </table>
              </div>
            ))}
            {showMoney && (
              <div className="flex justify-end text-sm border-t-2 border-[var(--leon-black)] pt-1">
                <span className="mr-4 font-bold">
                  {t ? `${t.code || t.name || 'Type'} total` : 'Unassigned total'} — {fmtCurrency(g.perUnit, boq.currency)} × {count}
                </span>
                <span className="font-bold">{fmtCurrency(g.extended, boq.currency)}</span>
              </div>
            )}
          </div>
        );
      })}

      {!(boq.lines || []).length && <EmptyState text="This bill has no lines yet." />}

      {showMoney && (boq.lines || []).length > 0 && (
        <div className="border-t-2 border-[var(--leon-black)] pt-2 text-sm">
          <div className="flex justify-end">
            <span className="mr-4 text-[var(--leon-black)]/60">One of each apartment type</span>
            <span className="w-40 text-right">{fmtCurrency(totals.perUnitAll, boq.currency)}</span>
          </div>
          <div className="flex justify-end font-bold text-base">
            <span className="mr-4">{totals.complete ? `Total — ${boq.currency}` : `Priced so far — ${boq.currency}`}</span>
            <span className="w-40 text-right">{fmtCurrency(totals.extendedAll, boq.currency)}</span>
          </div>
          <div className="flex justify-end font-bold text-base">
            <span className="mr-4">{`Total — ${boq.secondaryCurrency}`}</span>
            <span className="w-40 text-right">
              {totals.fxRate ? boqSecondaryText(boq, totals) : <span className="text-[#b83b3b] text-xs font-semibold">no rate entered</span>}
            </span>
          </div>
          <div className="text-right text-[11px] text-[var(--leon-black)]/55 mt-1">{boqFxSentence(boq)}</div>
          {!totals.complete && (
            <div className="text-right text-[11px] font-semibold text-[#b83b3b] mt-1">
              Incomplete — {unpriced.length} line{unpriced.length === 1 ? '' : 's'} carry no rate and contribute nothing to the figures above.
            </div>
          )}
        </div>
      )}

      {boq.notes && <div className="text-xs whitespace-pre-wrap text-[var(--leon-black)]/75">{boq.notes}</div>}
      {boq.terms && (
        <div className="text-xs">
          <div className="font-bold uppercase tracking-wide text-[10px] text-[var(--leon-black)]/50 mb-0.5">Terms</div>
          <div className="whitespace-pre-wrap text-[var(--leon-black)]/75">{boq.terms}</div>
        </div>
      )}
      <div className="text-[10px] text-[var(--leon-black)]/40">
        Quantities are entered per apartment and extended by the apartment count printed against each type. The
        exchange rate shown is the one held on this bill, not a market rate — the Hub has no market feed and does
        not look one up.
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════ revisions

function BoqRevisionsPanel({ ctx, project, account, boq, totals, families, editable, apply }) {
  const [note, setNote] = useState('');
  const [confirming, setConfirming] = useState(false);
  const [viewing, setViewing] = useState(null);
  const unpriced = boqUnpricedLines(boq);
  const revs = (boq.revisions || []).slice().sort((a, b) => (b.revision || 0) - (a.revision || 0));

  // Issuing FREEZES a copy. Everything the bill said on the day it was sent —
  // the lines, the apartment counts, the currencies and the exchange rate with
  // its stamp — is stored on the revision, so an issued bill can always be
  // produced again even after the live bill has moved on.
  function issue() {
    const t = boqTotals(boq);
    apply(b => {
      const n = (b.revision || 0) + 1;
      b.revision = n;
      b.status = n === 1 ? 'Issued' : 'Revised';
      b.revisions = b.revisions || [];
      b.revisions.push({
        id: uid('boqrev'), revision: n, date: todayISO(), by: ctx.currentUserName || '',
        note: note.trim(), status: b.status,
        snapshot: {
          unitTypes: cloneDeep(b.unitTypes || []), lines: cloneDeep(b.lines || []),
          currency: b.currency, secondaryCurrency: b.secondaryCurrency,
          fxRate: b.fxRate, fxDate: b.fxDate, notes: b.notes, terms: b.terms, name: b.name,
        },
        totals: { extendedAll: t.extendedAll, perUnitAll: t.perUnitAll, secondary: t.secondary, complete: t.complete, unpricedAll: t.unpricedAll },
      });
    }, `revision ${(boq.revision || 0) + 1} issued${note.trim() ? ` — ${note.trim()}` : ''}.`);
    setNote('');
    setConfirming(false);
  }

  const snapBoq = viewing ? Object.assign({}, boq, viewing.snapshot, { revision: viewing.revision, status: viewing.status }) : null;

  return (
    <div className="space-y-3">
      <BoqPanel title="Issue a revision">
        <div className="text-xs text-[var(--leon-black)]/70 mb-3">
          Issuing freezes a copy of the bill exactly as it stands — lines, apartment counts, currencies and the
          exchange rate with its stamp — so the document that went out can always be produced again. The live
          bill carries on from there.
        </div>
        <Field label="What changed in this revision" hint="Printed nowhere; it is the record of why a new revision exists.">
          <TextInput value={note} disabled={!editable} onChange={e => setNote(e.target.value)}
            placeholder="e.g. Type B wardrobes re-measured after the architect's Rev C" />
        </Field>
        {!editable && <div className="text-xs text-[var(--leon-black)]/50 mt-2">You have read-only access to LEON Softwares.</div>}
        {editable && !confirming && (
          <Button size="sm" className="mt-3" onClick={() => setConfirming(true)} disabled={!(boq.lines || []).length}>
            Issue revision {(boq.revision || 0) + 1}
          </Button>
        )}
        {editable && confirming && (
          <div className="mt-3">
            <ConfirmBar
              tone={totals.complete ? undefined : 'danger'}
              text={totals.complete
                ? `Freeze revision ${(boq.revision || 0) + 1} at ${fmtCurrency(totals.extendedAll, boq.currency)}?`
                : `${unpriced.length} line${unpriced.length === 1 ? '' : 's'} carry no rate. Issuing now sends a bill that does not total.`}
              confirmLabel={`Issue revision ${(boq.revision || 0) + 1}`}
              onConfirm={issue} onCancel={() => setConfirming(false)} />
          </div>
        )}
      </BoqPanel>

      <BoqPanel title="Issued revisions">
        {!revs.length && <EmptyState text="Nothing has been issued yet." />}
        {revs.length > 0 && (
          <table className="w-full text-sm">
            <thead>
              <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
                <th className="py-1 pr-2">Rev.</th>
                <th className="py-1 pr-2">Issued</th>
                <th className="py-1 pr-2">By</th>
                <th className="py-1 pr-2">Note</th>
                {ctx.canSeeFin && <th className="py-1 pr-2 text-right">Total</th>}
                <th className="py-1" />
              </tr>
            </thead>
            <tbody>
              {revs.map(r => (
                <tr key={r.id} className="border-b border-[var(--leon-line)] align-top">
                  <td className="py-1.5 pr-2 font-semibold">{r.revision}</td>
                  <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(r.date)}</td>
                  <td className="py-1.5 pr-2">{r.by || '—'}</td>
                  <td className="py-1.5 pr-2 text-xs">{r.note || <span className="text-[var(--leon-black)]/40">no note</span>}</td>
                  {ctx.canSeeFin && (
                    <td className="py-1.5 pr-2 text-right whitespace-nowrap">
                      {fmtCurrency(r.totals ? r.totals.extendedAll : 0, (r.snapshot && r.snapshot.currency) || boq.currency)}
                      {r.totals && !r.totals.complete && <div className="text-[10px] text-[#b83b3b]">issued incomplete</div>}
                    </td>
                  )}
                  <td className="py-1.5 text-right">
                    <Button size="sm" variant="ghost" onClick={() => setViewing(r)}>Produce it again</Button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </BoqPanel>

      <BoqPanel title="Activity">
        {!(boq.activity || []).length && <EmptyState text="Nothing recorded yet." />}
        <ul className="space-y-1 text-xs">
          {(boq.activity || []).slice(0, 40).map(a => (
            <li key={a.id} className="flex gap-2">
              <span className="text-[var(--leon-black)]/45 whitespace-nowrap">{fmtDate(a.date)}</span>
              <span className="text-[var(--leon-black)]/45 whitespace-nowrap">{a.by || '—'}</span>
              <span>{a.text}</span>
            </li>
          ))}
        </ul>
      </BoqPanel>

      <Modal open={!!viewing} onClose={() => setViewing(null)} wide
        title={viewing ? `${boq.name} — Revision ${viewing.revision}, issued ${fmtDate(viewing.date)}` : ''}
        footer={<Button variant="ghost" onClick={() => setViewing(null)}>Close</Button>}>
        {snapBoq && (
          <BoqDocument ctx={ctx} project={project} account={account} boq={snapBoq} totals={boqTotals(snapBoq)}
            families={families}
            actions={
              <div className="no-print flex items-center gap-1 justify-end">
                <Button size="sm" variant="outline" onClick={() => boqExportCsv(ctx, project, snapBoq)}>Download CSV</Button>
                <DocActions title={`${boq.name} Rev ${viewing.revision}`} heading="Bill of Quantities"
                  lines={[project.name, account ? account.name : '', `Revision ${viewing.revision}`]} />
              </div>
            } />
        )}
      </Modal>
    </div>
  );
}

// ═══════════════════════════════════════════════════ import from the take-off

// The take-off already carries the two things this bill needs — a unit type and
// a measurement — so re-typing them would be both slow and a chance to get them
// wrong. But NOTHING is imported silently: every row lands in a review table
// first, and where the mapping is ambiguous the field is left BLANK for a
// person to choose rather than guessed at.
function boqMapUnit(u) {
  const key = String(u || '').trim().toUpperCase();
  return BOQ_TAKEOFF_UNITS[key] || '';
}
function boqMatchType(text, types) {
  const s = String(text || '').trim().toLowerCase();
  if (!s) return '';
  const hit = types.find(t => String(t.code || '').trim().toLowerCase() === s)
    || types.find(t => String(t.name || '').trim().toLowerCase() === s)
    || types.find(t => s && String(t.name || '').trim().toLowerCase().indexOf(s) >= 0);
  return hit ? hit.id : '';
}
function boqMatchWorkItem(item, familyName, scopeLibrary) {
  const items = familyWorkItems(familyName, scopeLibrary) || [];
  if (!items.length) return '';
  const hay = [item.toolName, item.surface, item.room].filter(Boolean).map(s => String(s).toLowerCase());
  const hit = items.find(w => hay.indexOf(String(w).toLowerCase()) >= 0)
    || items.find(w => hay.some(h => h.indexOf(String(w).toLowerCase()) >= 0 || String(w).toLowerCase().indexOf(h) >= 0));
  return hit || '';
}

function BoqImportModal({ open, onClose, ctx, project, boq, families, apply }) {
  const [rows, setRows] = useState([]);

  useEffect(() => {
    if (!open) { setRows([]); return; }
    const types = boq.unitTypes || [];
    const scopes = project.scopes || [];
    const famNames = families.map(f => f.name);
    const src = (project.takeoffItems || []).filter(i => i.active !== false && (i.included || 'Included') === 'Included');
    setRows(src.map(i => {
      const scope = scopes.find(s => s.id === i.scopeId) || null;
      // A family the client's region does not sell is NOT carried over — a bill
      // may only be written against the scopes that region actually sells.
      const fam = scope && famNames.indexOf(scope.familyName) >= 0 ? scope.familyName : '';
      return {
        key: i.id,
        include: true,
        unitTypeId: boqMatchType(i.unitType, types),
        familyName: fam,
        workItem: fam ? boqMatchWorkItem(i, fam, ctx.scopeLibrary) : '',
        description: [i.toolName, i.room, i.surface].filter(Boolean).join(' — ') || i.code || 'Take-off item',
        spec: i.code || '',
        // rawMeasure is ONE apartment's measurement. The take-off's own typical
        // multiplier is deliberately NOT applied: this bill multiplies by the
        // apartment count itself, and applying both would double the quantity.
        qty: boqNum(i.rawMeasure),
        unit: boqMapUnit(i.unit),
        srcUnitType: i.unitType || '',
        srcUnit: i.unit || '',
        typicalNote: i.typical ? `take-off marked typical ×${boqNum(i.typicalCount) || 1}` : '',
      };
    }));
  }, [open, boq.id, project.id]);

  const ready = rows.filter(r => r.include && r.unitTypeId && r.familyName && r.workItem && r.unit);
  const blocked = rows.filter(r => r.include && !(r.unitTypeId && r.familyName && r.workItem && r.unit));
  const set = (key, field, value) => setRows(rs => rs.map(r => r.key === key
    ? Object.assign({}, r, { [field]: value }, field === 'familyName' ? { workItem: '' } : {})
    : r));

  function commit() {
    const made = ready.map(r => makeBoqLine({
      unitTypeId: r.unitTypeId, familyName: r.familyName, workItem: r.workItem,
      description: r.description, spec: r.spec, qty: boqNum(r.qty), unit: r.unit,
      rate: null,   // never guessed — a rate is a decision, not a measurement
      notes: r.typicalNote ? `From the take-off (${r.typicalNote}); quantity is for one apartment.` : 'From the take-off; quantity is for one apartment.',
    }));
    apply(b => { b.lines = (b.lines || []).concat(made); },
      `${made.length} line${made.length === 1 ? '' : 's'} imported from the take-off, all unpriced.`);
    onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Import from the take-off" footer={<>
      <Button variant="ghost" onClick={onClose}>Cancel</Button>
      <Button onClick={commit} disabled={!ready.length}>Add {ready.length} line{ready.length === 1 ? '' : 's'}</Button>
    </>}>
      <div className="space-y-3">
        <div className="text-xs text-[var(--leon-black)]/70">
          Nothing is added until you press the button. Quantities come across as the measurement for
          <strong> one apartment</strong> — the take-off's own typical multiplier is not applied, because this
          bill multiplies by the apartment count itself and applying both would double every quantity. No rate
          is ever brought over: a rate is a decision, not a measurement, so every imported line arrives unpriced.
        </div>
        {!rows.length && <EmptyState text="No take-off items on this job to import." />}
        {blocked.length > 0 && (
          <div className="text-xs text-[#a67b1f]">
            {blocked.length} row{blocked.length === 1 ? '' : 's'} still need an apartment type, scope, work item or
            unit. They are left blank rather than guessed at, and will not be imported until they are filled in.
          </div>
        )}
        {rows.length > 0 && (
          <div className="overflow-x-auto max-h-[50vh]">
            <table className="w-full text-xs">
              <thead>
                <tr className="text-left uppercase tracking-wide text-[10px] text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
                  <th className="py-1 pr-2">Use</th>
                  <th className="py-1 pr-2">Apartment type</th>
                  <th className="py-1 pr-2">Scope</th>
                  <th className="py-1 pr-2">Work item</th>
                  <th className="py-1 pr-2">Description</th>
                  <th className="py-1 pr-2 text-right">Qty / apt</th>
                  <th className="py-1 pr-2">Unit</th>
                </tr>
              </thead>
              <tbody>
                {rows.map(r => {
                  const items = familyWorkItems(r.familyName, ctx.scopeLibrary) || [];
                  return (
                    <tr key={r.key} className="border-b border-[var(--leon-line)] align-top">
                      <td className="py-1 pr-2">
                        <input type="checkbox" checked={r.include} onChange={e => set(r.key, 'include', e.target.checked)} />
                      </td>
                      <td className="py-1 pr-2">
                        <Select value={r.unitTypeId} onChange={e => set(r.key, 'unitTypeId', e.target.value)} className="!py-1 !text-xs">
                          <option value="">— choose —</option>
                          {(boq.unitTypes || []).map(t => <option key={t.id} value={t.id}>{t.code || t.name || 'Unnamed'}</option>)}
                        </Select>
                        {r.srcUnitType && <div className="text-[10px] text-[var(--leon-black)]/45 mt-0.5">take-off said "{r.srcUnitType}"</div>}
                      </td>
                      <td className="py-1 pr-2">
                        <Select value={r.familyName} onChange={e => set(r.key, 'familyName', e.target.value)} className="!py-1 !text-xs">
                          <option value="">— choose —</option>
                          {families.map(f => <option key={f.name} value={f.name}>{f.name}</option>)}
                        </Select>
                      </td>
                      <td className="py-1 pr-2">
                        <Select value={r.workItem} onChange={e => set(r.key, 'workItem', e.target.value)}
                          disabled={!r.familyName} className="!py-1 !text-xs">
                          <option value="">— choose —</option>
                          {items.map(w => <option key={w} value={w}>{w}</option>)}
                        </Select>
                      </td>
                      <td className="py-1 pr-2 min-w-[160px]">
                        <TextInput value={r.description} onChange={e => set(r.key, 'description', e.target.value)} className="!py-1 !text-xs" />
                      </td>
                      <td className="py-1 pr-2 w-20">
                        <TextInput type="number" step="any" value={r.qty} onChange={e => set(r.key, 'qty', e.target.value === '' ? 0 : Number(e.target.value))}
                          className="!py-1 !text-xs text-right" />
                      </td>
                      <td className="py-1 pr-2 w-24">
                        <Select value={r.unit} onChange={e => set(r.key, 'unit', e.target.value)} className="!py-1 !text-xs">
                          <option value="">— choose —</option>
                          {BOQ_UNITS.map(u => <option key={u} value={u}>{u}</option>)}
                        </Select>
                        {!r.unit && r.srcUnit && <div className="text-[10px] text-[#a67b1f] mt-0.5">"{r.srcUnit}" is not a bill unit</div>}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </Modal>
  );
}
