// ============================================================================
// LEON Operations Hub — application shell, screens, and state
// ============================================================================

// ---------------------------------------------------------------------------
// Scope-library helpers (admin-extensible catalog, §8.2)
// ---------------------------------------------------------------------------
function findFamilyById(lib, id) { return lib.find(f => f.id === id); }
function findCategoryPath(lib, catId) {
  for (const f of lib) { const c = f.categories.find(c => c.id === catId); if (c) return { family: f, category: c }; }
  return {};
}
function findOptionPath(lib, optId) {
  for (const f of lib) for (const c of f.categories) {
    const o = c.options.find(o => o.id === optId);
    if (o) return { family: f, category: c, option: o };
  }
  return {};
}
function moveItem(arr, id, dir) {
  const i = arr.findIndex(x => x.id === id);
  const j = i + dir;
  if (i < 0 || j < 0 || j >= arr.length) return;
  const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}

// Every Selections edit is recorded as a new revision set — a full snapshot
// of the scope's selections + selection areas at that moment, not just the
// one field that changed (§ selections request). The live scope.selections/
// selectionAreas stay the single source of truth for everything else that
// reads them (Documents, Submittals, profitability…); this is an append-only
// history alongside them.
function pushSelectionRevision(scope, change, actingUser, meeting) {
  scope.selectionRevisions = scope.selectionRevisions || [];
  const revisionNumber = (scope.selectionRevisions.length ? Math.max(...scope.selectionRevisions.map(r => r.revisionNumber)) : 0) + 1;
  scope.selectionRevisions.push({
    id: uid('selrev'), revisionNumber, date: todayISO(), changedBy: actingUser, change,
    selections: cloneDeep(scope.selections), selectionAreas: cloneDeep(scope.selectionAreas),
    // Where and with whom this version was settled, when it was settled in a
    // meeting rather than at a desk. Null for a revision locked on its own.
    meeting: meeting && meeting.held ? makeSelectionMeeting(meeting) : null,
  });
}

// ============================================================================
// Root App
// ============================================================================
function App() {
  const persisted = useMemo(() => loadPersistedState(), []);

  const [accounts, setAccounts] = useState(() => ((persisted && persisted.accounts) || SEED.accounts).map(normalizeAccount));
  // Export containers are a top-level collection (mirrors warehouses/
  // materialAllocations/logisticsClaims) so one container can carry material
  // for several projects/scopes at once via its shipments array (§ multi-
  // project containers), instead of being architecturally locked to one
  // project. This must read the RAW persisted/SEED projects before the
  // `projects` state below runs normalizeProject on them — normalizeProject
  // deletes each project's legacy nested exportContainers in place, so
  // reading it after that would find nothing to migrate.
  const [exportContainers, setExportContainers] = useState(() => {
    if (persisted && persisted.exportContainers) return persisted.exportContainers.map(normalizeExportContainer);
    const legacySource = (persisted && persisted.projects) || SEED.projects;
    return legacySource.flatMap(p => (p.exportContainers || []).map(c => normalizeExportContainer({
      ...c, shipments: [{ projectId: p.id, scopeIds: c.scopeIds || [] }],
    })));
  });
  const [projects, setProjects] = useState(() => {
    if (persisted && persisted.projects) return persisted.projects.map(normalizeProject);
    // apply the seeded delay-cascade demo through the real cascade function once on first load
    return SEED.projects.map(p => {
      if (p.__pendingDelayDemo) {
        const d = p.__pendingDelayDemo;
        const clean = { ...p };
        delete clean.__pendingDelayDemo;
        const withDelay = applyDelayCascade(clean, d.scopeId, d.stageId ?? clean.scopes.find(s => s.id === d.scopeId).stages[d.stageIndex].id, d.days, d.reason, d.note, 'Project Coordinator', 'Rachel Kim');
        withDelay.health = computeProjectHealth(withDelay);
        return withDelay;
      }
      const clean = { ...p };
      delete clean.__pendingDelayDemo;
      clean.health = computeProjectHealth(clean);
      return clean;
    }).map(normalizeProject);
  });
  const [scopeLibrary, setScopeLibrary] = useState(() => (persisted && persisted.scopeLibrary) ? mergeNewScopeFamilies(persisted.scopeLibrary) : repairOptionImages(DEFAULT_SCOPE_LIBRARY));
  const [teamDirectory, setTeamDirectory] = useState(() => (persisted && persisted.teamDirectory ? mergeNewTeamMembers(persisted.teamDirectory) : TEAM_DIRECTORY).map(normalizeTeamMember));
  const [documentLibrary, setDocumentLibrary] = useState(() => mergeNewLibraryDocs((persisted && persisted.documentLibrary) || SEED.documentLibrary));
  // The Contractor block on the AIA print package needs LEON's own company
  // name/address — company-wide, not per-project, so it lives alongside
  // documentLibrary/materialLibrary rather than duplicated into every job.
  const [companyProfile, setCompanyProfile] = useState(() => (persisted && persisted.companyProfile) ? { ...makeCompanyProfile(), ...persisted.companyProfile } : makeCompanyProfile());
  // Every finish catalog is bought from someone, so the vendor records and the
  // links are built together on first load — see seedSupplierVendors.
  const __supplierSeed = useMemo(() => seedSupplierVendors(
    ((persisted && persisted.vendors) || SEED.vendors).map(normalizeVendor),
    (persisted && persisted.supplierVendorLinks) || null), []);
  const [vendors, setVendors] = useState(() => __supplierSeed.vendors);
  const [freightForwarders, setFreightForwarders] = useState(() => ((persisted && persisted.freightForwarders) || SEED.freightForwarders).map(normalizeVendor));
  const [materialLibrary, setMaterialLibrary] = useState(() => (persisted && persisted.materialLibrary) || SEED.materialLibrary);
  // Supporting documents filed against a scope family (and optionally one of
  // its selection categories). Seeded once from whatever was already attached
  // to material records — see seedScopeDocuments.
  const [scopeDocuments, setScopeDocuments] = useState(() => seedScopeDocuments(
    (persisted && persisted.materialLibrary) || SEED.materialLibrary,
    (persisted && persisted.scopeDocuments) || null));
  function addScopeDocument(data) { setScopeDocuments(prev => [...prev, makeScopeDocument(data, currentUserName)]); }
  function updateScopeDocument(id, fields) { setScopeDocuments(prev => prev.map(d => d.id === id ? { ...d, ...fields } : d)); }
  function removeScopeDocument(id) { setScopeDocuments(prev => prev.map(d => d.id === id ? { ...d, active: false } : d)); }
  const [applianceLibrary, setApplianceLibrary] = useState(() => (persisted && persisted.applianceLibrary) || []);
  const [fixtureLibrary, setFixtureLibrary] = useState(() => (persisted && persisted.fixtureLibrary) || []);
  const [windowLeadTimeLibrary, setWindowLeadTimeLibrary] = useState(() => (persisted && persisted.windowLeadTimeLibrary) ? mergeNewWindowLeadTimeEntries(persisted.windowLeadTimeLibrary) : DEFAULT_WINDOW_LEAD_TIME_LIBRARY);
  function addWindowLeadTimeEntry(data) {
    setWindowLeadTimeLibrary(prev => [...prev, makeWindowLeadTimeEntry(data)]);
  }
  function updateWindowLeadTimeEntry(entryId, fields) {
    setWindowLeadTimeLibrary(prev => prev.map(e => e.id === entryId ? { ...e, ...fields } : e));
  }
  function setWindowLeadTimeEntryActive(entryId, active) {
    setWindowLeadTimeLibrary(prev => prev.map(e => e.id === entryId ? { ...e, active } : e));
  }
  // Edits and deletions applied to the shipped supplier catalogs. Sparse and
  // keyed "<supplier>:<id>" — see supplierCatalog() in data.jsx for why this
  // is kept separate from the catalog files themselves.
  const [supplierFinishOverrides, setSupplierFinishOverrides] = useState(() => (persisted && persisted.supplierFinishOverrides) || {});
  // Which vendor each finish catalog belongs to. Registry-backed for the same
  // reason as role permissions: makeSupplierFinishRef is pure and has no way
  // to reach React state.
  const [supplierVendorLinks, setSupplierVendorLinks] = useState(() => __supplierSeed.links);
  // Finishes the team imported themselves — see makeImportedFinish. Registry-
  // backed like the overrides, because supplierCatalog() is a pure function.
  const [importedFinishes_, setImportedFinishes] = useState(() => (persisted && persisted.importedFinishes) || []);
  setActiveImportedFinishes(importedFinishes_);
  function addImportedFinishes(rows) {
    const made = rows.map(r => makeImportedFinish({ ...r, importedBy: currentUserName }));
    setImportedFinishes(prev => [...prev, ...made]);
    return made.length;
  }
  function removeImportedFinishBatch(sup, importedDate) {
    setImportedFinishes(prev => prev.filter(r => !(r.sup === sup && r.importedDate === importedDate)));
  }
  setActiveSupplierVendorLinks(supplierVendorLinks);
  function linkSupplierVendor(supKey, vendorId) {
    setSupplierVendorLinks(prev => { const next = { ...prev }; if (vendorId) next[supKey] = vendorId; else delete next[supKey]; return next; });
  }
  // Point the module-level registry at the live map before any child renders,
  // exactly as rolePermissions does below.
  setActiveSupplierOverrides(supplierFinishOverrides);
  function updateSupplierFinish(sup, id, fields) {
    setSupplierFinishOverrides(prev => ({ ...prev, [supplierFinishKey(sup, id)]: { ...(prev[supplierFinishKey(sup, id)] || {}), ...fields } }));
  }
  function setSupplierFinishHidden(sup, id, hidden) {
    if (hidden) { updateSupplierFinish(sup, id, { hidden: true }); return; }
    // Un-hiding a record that carries no other edits drops the override
    // entirely, rather than leaving an inert {hidden:false} behind.
    setSupplierFinishOverrides(prev => {
      const key = supplierFinishKey(sup, id);
      const rest = { ...(prev[key] || {}) };
      delete rest.hidden;
      const next = { ...prev };
      if (Object.keys(rest).length) next[key] = rest; else delete next[key];
      return next;
    });
  }
  // Drops the override entirely, restoring the supplier's published record.
  function resetSupplierFinish(sup, id) {
    setSupplierFinishOverrides(prev => {
      const next = { ...prev };
      delete next[supplierFinishKey(sup, id)];
      return next;
    });
  }
  // Manually-entered cash events — company overhead (rent, payroll, insurance,
  // utilities) and other money with no job record behind it. Everything else on
  // the Financial Calendar is derived; see buildCashEvents (lib.jsx).
  const [cashEntries, setCashEntries] = useState(() => (persisted && persisted.cashEntries) || []);
  const [cashSettings, setCashSettings] = useState(() => (persisted && persisted.cashSettings) || makeCashSettings());
  const [creditCards, setCreditCards] = useState(() => (persisted && persisted.creditCards) || []);
  // Notifications: recorded events (see NOTIFICATION_EVENTS in data.jsx) plus
  // the outbox of emails those events asked for, held until there is a service
  // to send them.
  const [notifications, setNotifications] = useState(() => (persisted && persisted.notifications) || []);
  const [notificationPrefs, setNotificationPrefs] = useState(() => (persisted && persisted.notificationPrefs) || {});
  const [emailOutbox, setEmailOutbox] = useState(() => (persisted && persisted.emailOutbox) || []);
  // The share log — what was sent to whom, by whom, when.
  const [shares, setShares] = useState(() => (persisted && persisted.shares) || []);
  function updateCashSettings(fields) { setCashSettings(prev => ({ ...prev, ...fields })); }
  // Reset the running balance at one week without touching the weeks before it.
  function setWeekOpeningBalance(weekStart, amount) {
    setCashSettings(prev => {
      const next = { ...prev, weekOverrides: { ...(prev.weekOverrides || {}) } };
      if (amount == null || amount === '') delete next.weekOverrides[weekStart];
      else next.weekOverrides[weekStart] = Number(amount) || 0;
      return next;
    });
  }
  function addCreditCard(data) { setCreditCards(prev => [...prev, makeCreditCard(data)]); }
  function updateCreditCard(id, fields) { setCreditCards(prev => prev.map(c => c.id === id ? { ...c, ...fields } : c)); }
  function removeCreditCard(id) { setCreditCards(prev => prev.map(c => c.id === id ? { ...c, active: false } : c)); }
  function addCashEntry(data) { setCashEntries(prev => [...prev, makeCashEntry(data, currentUserName)]); }
  function updateCashEntry(id, fields) { setCashEntries(prev => prev.map(e => e.id === id ? { ...e, ...fields } : e)); }
  function removeCashEntry(id) { setCashEntries(prev => prev.map(e => e.id === id ? { ...e, active: false } : e)); }

  // Payment-term planning. expectedDate null means "follow the schedule".
  function setPaymentTermPlan(projectId, termId, fields) {
    updateProject(projectId, draft => {
      const t = (draft.paymentTerms || []).find(x => x.id === termId);
      if (t) Object.assign(t, fields);
    });
  }
  // Push a planned date out without losing what it was originally promised for
  // — the first move is remembered, so "this has already slipped twice" stays
  // visible instead of the plan quietly rewriting itself.
  function postponePaymentTerm(projectId, termId, newDate, reason) {
    updateProject(projectId, draft => {
      const t = (draft.paymentTerms || []).find(x => x.id === termId);
      if (!t) return;
      const was = t.expectedDate || paymentTermDate(draft, t).date;
      const next = newDate || null;                // null = go back to following the schedule
      if (was === next) return;                    // nothing moved
      if (!t.expectedDateOriginal) t.expectedDateOriginal = was || null;
      t.expectedDate = next;
      t.expectedDateHistory = [...(t.expectedDateHistory || []), { from: was || null, to: next, reason: reason || '', date: todayISO(), by: currentUserName }];
      logAction(draft, next
        ? `Expected client payment for ${t.label} moved${was ? ` from ${was}` : ''} to ${next}${reason ? ` — ${reason}` : ''}.`
        : `Expected client payment for ${t.label} released back to the schedule (was ${was}).`);
      draft.__notify = [...(draft.__notify || []), {
        event: 'payment.moved',
        toUserIds: projectWatchers(projectId),
        title: `Expected payment moved — ${t.label}`,
        body: `${currentUserName} moved the expected client payment${was ? ` from ${was}` : ''} to ${next || 'the schedule'}${reason ? ` — ${reason}` : ''}.`,
      }];
    });
    drainDraftNotices(projectId);
  }
  function postponeApInvoiceDue(projectId, invoiceId, newDate, reason) {
    updateProject(projectId, draft => {
      const inv = (draft.apInvoices || []).find(i => i.id === invoiceId);
      if (!inv) return;
      const was = inv.dueDate || inv.invoiceDate || null;
      if (was === newDate) return;                 // nothing moved
      if (!inv.dueDateOriginal) inv.dueDateOriginal = was;
      inv.dueDate = newDate;
      inv.dueDateHistory = [...(inv.dueDateHistory || []), { from: was, to: newDate, reason: reason || '', date: todayISO(), by: currentUserName }];
      logAction(draft, `Payment date for invoice ${inv.invoiceNumber} (${inv.vendorName || 'vendor'}) moved${was ? ` from ${was}` : ''} to ${newDate}${reason ? ` — ${reason}` : ''}.`);
      draft.__notify = [...(draft.__notify || []), {
        event: 'payment.moved',
        toUserIds: projectWatchers(projectId),
        title: `Vendor payment moved — ${inv.vendorName || inv.invoiceNumber}`,
        body: `${currentUserName} moved invoice ${inv.invoiceNumber}${was ? ` from ${was}` : ''} to ${newDate}${reason ? ` — ${reason}` : ''}.`,
      }];
    });
    drainDraftNotices(projectId);
  }
  // ---- notifications -------------------------------------------------------
  // ONE entry point. Every trigger in the app calls this and nothing else, so
  // the routing rules (who wants what, on which channel) live in exactly one
  // place and adding a trigger later can't accidentally bypass them.
  function notify(eventKey, { toUserIds, title, body, projectId, link }) {
    const project = projectId ? projects.find(p => p.id === projectId) : null;
    // Never notify someone about their own action — they just did it.
    const ids = [...new Set((toUserIds || []).filter(Boolean))].filter(id => id !== effectiveUserId);
    if (!ids.length) return;
    const made = [];
    const mails = [];
    ids.forEach(toUserId => {
      const prefs = notificationPrefs[toUserId] || {};
      if (!wantsNotification(prefs, eventKey, 'inApp') && !wantsNotification(prefs, eventKey, 'email')) return;
      const n = makeNotification({
        event: eventKey, title, body, toUserId, byUser: currentUserName,
        projectId: projectId || null, projectName: project ? project.name : '',
        link: link || (projectId ? { view: 'project', projectId } : null),
      });
      if (wantsNotification(prefs, eventKey, 'inApp')) made.push(n);
      if (wantsNotification(prefs, eventKey, 'email')) {
        const person = teamDirectory.find(p => p.id === toUserId);
        mails.push(makeQueuedEmail(n, prefs.emailAddress || (person && person.email) || ''));
      }
    });
    if (made.length) setNotifications(prev => [...made, ...prev].slice(0, 500));
    if (mails.length) setEmailOutbox(prev => [...mails, ...prev].slice(0, 500));
  }
  // ---- sharing ------------------------------------------------------------
  // "Share" is deliberately not a mail client. It answers one question — who
  // should see this — and then does the two things it honestly can: colleagues
  // get it in their inbox immediately, and anyone with an email address gets a
  // message queued for the mail service. Nothing here pretends to have sent.
  function shareItem({ subject, summary, link, projectId, recipients, message, subjectKey, items, itemsTotal }) {
    const people = (recipients || []).filter(r => r && (r.userId || r.email));
    if (!people.length) return { inApp: 0, queued: 0 };
    const project = projectId ? projects.find(p => p.id === projectId) : null;
    // The list of what is included goes IN the message, so the recipient sees
    // it whether they read this in the app or in an email.
    const manifest = (items && items.length)
      ? `Included (${items.length}${itemsTotal ? ` of ${itemsTotal}` : ''}):\n` + items.map(i => `\u2022 ${i.group ? `${i.group} — ` : ''}${i.label}`).join('\n')
      : '';
    const body = [message, summary, manifest].filter(Boolean).join('\n\n');
    const internal = people.filter(r => r.userId);
    const made = [];
    const mails = [];
    // The sender is copied on everything that goes out. A share to a client is
    // correspondence they are responsible for, and the copy in their own
    // mailbox is where they keep it.
    const senderEmail = (currentUser && currentUser.email) || '';
    internal.forEach(r => {
      const n = makeNotification({
        event: 'share.received',
        title: `${currentUserName} shared: ${subject}`,
        body, toUserId: r.userId, byUser: currentUserName,
        projectId: projectId || null, projectName: project ? project.name : '',
        link: link || (projectId ? { view: 'project', projectId } : null),
      });
      const prefs = notificationPrefs[r.userId] || {};
      // A share is a direct, deliberate act — it always reaches the person's
      // inbox. Only the EMAIL copy is subject to their preferences.
      made.push(n);
      if (wantsNotification(prefs, 'share.received', 'email')) {
        const person = teamDirectory.find(p => p.id === r.userId);
        const em = buildShareEmail({
          recipientName: r.name, senderName: currentUserName, senderTitle: personTitle(currentUser),
          senderSignature: currentUser && currentUser.emailSignature,
          senderSignatureImage: currentUser && currentUser.emailSignatureImage,
          subject, summary, message, items, itemsTotal,
          projectName: project ? project.name : '', company: companyProfile, external: !!r.portal,
        });
        mails.push({ ...makeQueuedEmail(n, prefs.emailAddress || r.email || (person && person.email) || '', senderEmail), subject: em.subject, body: em.body, html: em.html });
      }
    });
    // Anyone outside the team — a client contact, a vendor — has no inbox here,
    // so email is the only channel and it queues.
    people.filter(r => !r.userId && r.email).forEach(r => {
      const n = makeNotification({
        event: 'share.received', title: `${currentUserName} shared: ${subject}`,
        body, toUserId: null, byUser: currentUserName,
        projectId: projectId || null, projectName: project ? project.name : '',
      });
      const em = buildShareEmail({
        recipientName: r.name, senderName: currentUserName, senderTitle: personTitle(currentUser),
        senderSignature: currentUser && currentUser.emailSignature,
          senderSignatureImage: currentUser && currentUser.emailSignatureImage,
        subject, summary, message, items, itemsTotal,
        projectName: project ? project.name : '', company: companyProfile, external: true,
      });
      mails.push({ ...makeQueuedEmail(n, r.email, senderEmail), subject: em.subject, body: em.body, html: em.html });
    });
    if (made.length) setNotifications(prev => [...made, ...prev].slice(0, 500));
    if (mails.length) setEmailOutbox(prev => [...mails, ...prev].slice(0, 500));
    // The record of the share itself, independent of whether the email ever
    // sends — this is what answers "did anyone send this to the client?".
    setShares(prev => [makeShare({
      subjectKey, subject, summary, message, items: items || [], itemsTotal: itemsTotal != null ? itemsTotal : null,
      projectId: projectId || null, projectName: project ? project.name : '',
      by: currentUserName, byUserId: effectiveUserId,
      recipients: people.map(r => ({
        name: r.name, email: r.email || null, userId: r.userId || null,
        channel: r.userId ? 'inbox' : 'email',
      })),
    }), ...prev].slice(0, 1000));
    if (projectId) updateProject(projectId, draft => {
      logAction(draft, `Shared "${subject}" with ${people.map(r => r.name).join(', ')}.`);
    });
    return { inApp: made.length, queued: mails.length };
  }
  function markNotificationRead(id, read) {
    setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: read !== false } : n));
  }
  function markAllNotificationsRead() {
    setNotifications(prev => prev.map(n => n.toUserId === effectiveUserId ? { ...n, read: true } : n));
  }
  function clearReadNotifications() {
    setNotifications(prev => prev.filter(n => !(n.toUserId === effectiveUserId && n.read)));
  }
  function setMyNotificationPrefs(fields) {
    setNotificationPrefs(prev => ({ ...prev, [effectiveUserId]: { ...makeNotificationPrefs(), ...(prev[effectiveUserId] || {}), ...fields } }));
  }
  function setNotificationChannel(eventKey, channel, on) {
    setNotificationPrefs(prev => {
      const cur = { ...makeNotificationPrefs(), ...(prev[effectiveUserId] || {}) };
      cur[channel] = { ...(cur[channel] || {}), [eventKey]: on };
      return { ...prev, [effectiveUserId]: cur };
    });
  }
  function markOutboxSent(ids) {
    setEmailOutbox(prev => prev.map(m => ids.includes(m.id) ? { ...m, status: 'Sent', sentAt: new Date().toISOString() } : m));
  }
  // Who should hear about something happening on a project: the people
  // actually staffed on it, in the departments that project touches.
  function projectWatchers(projectId, extraIds) {
    const p = projects.find(x => x.id === projectId);
    const ids = [...(extraIds || [])];
    if (p) projectDepartments(p).forEach(dep => {
      const team = projectTeamFor(p, dep) || {};
      Object.values(team).forEach(v => { if (typeof v === 'string') ids.push(v); });
    });
    return ids;
  }

  // ---- client receipts, and the bank hold that can sit on top of them -------
  // Payment terms and AIA requisitions carry the SAME receipt/bank-hold shape,
  // so every mutator here takes a `kind` and resolves the record once rather
  // than existing twice.
  function findReceivable(draft, kind, id) {
    const list = kind === 'requisition' ? (draft.paymentRequisitions || []) : (draft.paymentTerms || []);
    return list.find(x => x.id === id);
  }
  function receivableLabel(kind, rec) {
    return kind === 'requisition'
      ? `Requisition R${rec.revision}${rec.reference ? ` — ${rec.reference}` : ''}`
      : rec.label;
  }
  // Which client-payment stage a receipt finishes. The stages live per SCOPE
  // but a payment term is project-level, so on a multi-scope job there is no
  // one right answer and the app says nothing rather than guessing.
  const CLIENT_PAYMENT_STAGE_ORDER = ['contract_deposit', 'production_completion_payment', 'delivery_jobsite_payment', 'final_payment'];
  function suggestClientPaymentStage(projectId, label) {
    const proj = projects.find(p => p.id === projectId);
    if (!proj || (proj.scopes || []).length !== 1) return;
    const scopeId = proj.scopes[0].id;
    const key = CLIENT_PAYMENT_STAGE_ORDER.find(k => findStageToSuggest(proj, scopeId, k));
    if (key) suggestStage(projectId, { scopeId, stageKey: key, because: label });
  }
  function recordClientReceipt(projectId, kind, id, { date, amount, reference }) {
    updateProject(projectId, draft => {
      const rec = findReceivable(draft, kind, id);
      if (!rec) return;
      rec.receivedDate = date || todayISO();
      rec.receivedAmount = amount != null ? Number(amount) : null;
      rec.receiptReference = reference || '';
      // A requisition's status tracks its APPROVAL (Submitted/Approved/…), not
      // payment, so only a payment term flips status here.
      if (kind !== 'requisition') rec.status = 'Paid';
      logAction(draft, `Client payment received — ${receivableLabel(kind, rec)} (${fmtMoney(rec.receivedAmount || 0)}) on ${rec.receivedDate}${reference ? ` · ref ${reference}` : ''}.`);
    });
    notify('payment.received', {
      toUserIds: projectWatchers(projectId), projectId,
      title: `Client payment recorded — ${fmtMoney(Number(amount) || 0)}`,
      body: `${currentUserName} recorded a client payment of ${fmtMoney(Number(amount) || 0)} received ${date || todayISO()}.`,
      link: { view: 'project', projectId, tab: 'financials' },
    });
    suggestClientPaymentStage(projectId, `A client payment of ${fmtMoney(Number(amount) || 0)} was recorded`);
  }
  // Setting the hold replaces the whole phase list in one commit — the modal
  // edits a draft and saves it whole, so there is no half-allocated state.
  function setBankHold(projectId, kind, id, hold) {
    updateProject(projectId, draft => {
      const rec = findReceivable(draft, kind, id);
      if (!rec) return;
      const label = receivableLabel(kind, rec);
      if (!hold) {
        rec.bankHold = null;
        logAction(draft, `Bank hold removed from ${label} — the payment now shows as available on the day it was received.`);
        return;
      }
      rec.bankHold = hold;
      const n = (hold.releases || []).length;
      logAction(draft, `${label} marked paid — held by ${hold.bankName || 'the bank'}, releasing in ${n} phase${n === 1 ? '' : 's'} (${fmtMoney(bankHoldAllocated(hold))}).`);
    });
  }
  // The bank actually let a phase go — the amount can differ from what was
  // planned, and that difference is exactly what this is here to surface.
  function recordBankRelease(projectId, kind, id, releaseId, { releasedDate, releasedAmount, note }) {
    updateProject(projectId, draft => {
      const rec = findReceivable(draft, kind, id);
      const rel = rec && rec.bankHold && (rec.bankHold.releases || []).find(r => r.id === releaseId);
      if (!rel) return;
      rel.releasedDate = releasedDate || todayISO();
      rel.releasedAmount = releasedAmount != null ? Number(releasedAmount) : Number(rel.amount) || 0;
      if (note) rel.note = note;
      logAction(draft, `Bank released ${fmtMoney(rel.releasedAmount)} of ${receivableLabel(kind, rec)} (${rel.name || 'release'}) on ${rel.releasedDate}.`);
      draft.__notify = [...(draft.__notify || []), {
        event: 'payment.received',
        toUserIds: projectWatchers(projectId),
        title: `Bank released ${fmtMoney(rel.releasedAmount)}`,
        body: `${rel.name || 'A release'} on ${receivableLabel(kind, rec)} was released on ${rel.releasedDate}.`,
      }];
    });
    drainDraftNotices(projectId);
  }
  function undoBankRelease(projectId, kind, id, releaseId) {
    updateProject(projectId, draft => {
      const rec = findReceivable(draft, kind, id);
      const rel = rec && rec.bankHold && (rec.bankHold.releases || []).find(r => r.id === releaseId);
      if (!rel) return;
      rel.releasedDate = null; rel.releasedAmount = null;
      logAction(draft, `Bank release "${rel.name || 'release'}" on ${receivableLabel(kind, rec)} reopened — back to a forecast.`);
    });
  }
  // Moving a planned release date is a forecast change like any other, so it
  // is versioned the same way.
  function postponeBankRelease(projectId, kind, id, releaseId, newDate, reason) {
    updateProject(projectId, draft => {
      const rec = findReceivable(draft, kind, id);
      const rel = rec && rec.bankHold && (rec.bankHold.releases || []).find(r => r.id === releaseId);
      if (!rel || rel.plannedDate === newDate) return;
      const was = rel.plannedDate || null;
      if (!rel.plannedDateOriginal) rel.plannedDateOriginal = was;
      rel.plannedDate = newDate;
      rel.plannedDateHistory = [...(rel.plannedDateHistory || []), { from: was, to: newDate, reason: reason || '', date: todayISO(), by: currentUserName }];
      logAction(draft, `Bank release "${rel.name || 'release'}" on ${receivableLabel(kind, rec)} moved${was ? ` from ${was}` : ''} to ${newDate}${reason ? ` — ${reason}` : ''}.`);
    });
  }
  function recordPaymentTermReceipt(projectId, termId, date, amount) {
    recordClientReceipt(projectId, 'paymentTerm', termId, { date, amount });
  }

  // Editable per-role permission defaults (Users -> Role Permissions).
  // Seeded from the previously-hardcoded constants, so an untouched install
  // behaves exactly as before.
  const [rolePermissions, setRolePermissions] = useState(() => mergeNewRolePermissions(persisted && persisted.rolePermissions));
  const [permissionLog, setPermissionLog] = useState(() => (persisted && persisted.permissionLog) || []);
  // Point the module-level registry (data.jsx) at the live map on every
  // render, BEFORE any child renders and calls a can*() gate. Assignment is
  // idempotent, so doing it in the render body is safe and keeps the gates
  // callable from the hundreds of places that have no access to React state.
  setActiveRolePermissions(rolePermissions);
  // Recovery guard: Admin must always keep edit access to the Users module.
  // Without it, an admin who denies it has no way back into this very editor
  // to undo the change — the app would need its localStorage cleared by hand.
  // Every other Admin permission stays freely editable.
  // WHO CHANGED WHAT, AND WHEN. Every other consequential edit in this app
  // leaves a trail — a project has a change log, a lead-time library has
  // revisions, a forecast date records every move. Permissions had NONE, and
  // that gap was found the hard way: when an admin's assignments turned out
  // to be missing there was no way to tell whether they had been overwritten,
  // never saved, or never made. `permissionLog` is that trail, capped like
  // the sign-in log, and it records the previous value as well as the new one
  // so a change can be read back and undone by hand.
  function logPermissionChange(entry) {
    setPermissionLog(prev => [{
      id: uid('plog'), date: new Date().toISOString(),
      by: currentUserName, byId: effectiveUserId, ...entry,
    }, ...prev].slice(0, PERMISSION_LOG_LIMIT));
  }
  function setRoleModulePermission(role, moduleKey, level) {
    if (role === 'Admin' && moduleKey === 'users' && level !== 'edit') return;
    const was = roleModuleLevel(role, moduleKey);
    if (was === level) return;
    logPermissionChange({ kind: 'module', role, key: moduleKey, from: was, to: level });
    setRolePermissions(prev => ({ ...prev, [role]: { ...prev[role], modules: { ...prev[role].modules, [moduleKey]: level } } }));
  }
  function setRoleCapability(role, capabilityKey, on) {
    const was = !!roleHasCapability(role, capabilityKey);
    if (was === !!on) return;
    logPermissionChange({ kind: 'capability', role, key: capabilityKey, from: was, to: !!on });
    setRolePermissions(prev => ({ ...prev, [role]: { ...prev[role], capabilities: { ...prev[role].capabilities, [capabilityKey]: !!on } } }));
  }
  // Restores ONE role to its seeded defaults, leaving every other role's
  // tuning alone.
  function resetRolePermissions(role) {
    const defaults = buildDefaultRolePermissions();
    logPermissionChange({ kind: 'reset', role, key: '(whole role)', from: 'tuned', to: 'defaults' });
    setRolePermissions(prev => ({ ...prev, [role]: defaults[role] }));
  }
  function resetAllRolePermissions() {
    logPermissionChange({ kind: 'resetAll', role: '(every role)', key: '(whole matrix)', from: 'tuned', to: 'defaults' });
    setRolePermissions(buildDefaultRolePermissions());
  }
  // Interiors Lead-Time Library — the Interiors counterpart of the window
  // library above. One entry per interiors scope family, holding that
  // family's own per-stage base durations (see makeInteriorLeadTimeEntry).
  // Public holidays across the six countries LEON works in. Everyone sees
  // them — a closed factory or port moves a date regardless of department.
  const [holidays, setHolidays] = useState(() => mergeNewHolidays(persisted && persisted.holidays));
  function addHoliday(data) { setHolidays(prev => [...prev, makeHoliday(data)]); }
  // Editing marks the row as the team's own, so a future regeneration of the
  // shipped table replaces the untouched rows around it and leaves this one be.
  function updateHoliday(id, fields) { setHolidays(prev => prev.map(x => x.id === id ? { ...x, ...fields, edited: true } : x)); }
  function removeHoliday(id) { setHolidays(prev => prev.map(x => x.id === id ? { ...x, active: false } : x)); }
  const [complexityLevels, setComplexityLevels] = useState(() => mergeNewComplexityLevels(persisted && persisted.complexityLevels));
  // ── LEON Softwares ──────────────────────────────────────────────────────
  // Each in-app software keeps ONE library state key rather than a collection
  // per object type — six separate keys would each need persisting, merging and
  // remembering, and they are never useful apart.
  // LEON Office — one store for Word, Sheets and Presentation. See
  // makeOfficeDocument: documents reference LEON assets, never copy them,
  // because localStorage caps at roughly 13 MB on this browser.
  const [officeDocs, setOfficeDocs] = useState(() => (persisted && persisted.officeDocs) || []);
  // Countertop price lists are a COMPANY record, not a per-quote one — the same
  // list prices every job until someone revises it, which is why it lives here
  // and not on a project. The quotes themselves hang off their project
  // (project.countertopQuotes) and persist with it.
  // The revision log for the company's DEFAULT stage schedule. Edits themselves
  // persist live, like everything else here — this records who revised the
  // defaults, when, and what moved, which is a different question from whether
  // the change was saved.
  // Work done before there is a job to attach it to. Deliberately a SEPARATE
  // collection from `projects` — see makeScratchProject for why a flag on the
  // main list would eventually be forgotten by one report and counted as real.
  const [scratchProjects, setScratchProjects] = useState(() => (persisted && persisted.scratchProjects) || []);
  const [leadTimeRevisions, setLeadTimeRevisions] = useState(() => (persisted && persisted.leadTimeRevisions) || []);
  const [ctPriceLists, setCtPriceLists] = useState(() => (persisted && persisted.ctPriceLists) || []);
  // The fenestration profile library lives in its own module (it is read
  // synchronously from pure helpers), so App owns only its persistence: hydrate
  // it once, then mirror every change back into state so it is saved.
  // Per-software settings. Sparse: only what an admin actually changed.
  const [softwareSettings, setSoftwareSettings] = useState(() => (persisted && persisted.softwareSettings) || {});
  // Re-pointed on every render, the same registry pattern as rolePermissions —
  // softwareSetting() is called from pure helpers with no route to React state.
  setActiveSoftwareSettings(softwareSettings);
  // The company's quote defaults — container capacities, freight legs, slab
  // yield and waste, commission. Forward-merged from the seed so a scope added
  // later still arrives, and re-pointed here because quoteRecipeFor() is pure.
  const [quoteRecipes, setQuoteRecipes] = useState(() => mergeNewQuoteRecipes(persisted && persisted.quoteRecipes));
  setActiveQuoteRecipes(quoteRecipes);
  const [quoteSpecs, setQuoteSpecs] = useState(() => mergeNewQuoteSpecs(persisted && persisted.quoteSpecs));
  // Which specification fields carry their PICTURE through to the client
  // quote and the presentation. Sparse and absence-means-all: a scope with no
  // entry shows every field that has a finish picked, which is the useful
  // default; an entry is an explicit curation, and an empty array is a real
  // answer meaning "none of them". Same rule as every other override map here —
  // a value equal to the default is not stored, so a better default still
  // reaches anyone who has not curated.
  const [quoteSpecImages, setQuoteSpecImages] = useState(() => (persisted && persisted.quoteSpecImages) || {});
  // The terms a quotation goes out with. VERSIONED, because an issued
  // quotation must keep the wording the client actually agreed to — editing
  // the standard raises a new version rather than rewriting history. Seeded
  // from QUOTE_TERMS_SEED (Leon's own 15 clauses) so a quotation can be issued
  // on day one without anyone having to type them in first.
  // Break-page artwork and the reference list. SPARSE and null-means-shipped:
  // an entry exists only where LEON has changed the standard, so an improvement
  // to the shipped set still reaches anyone who has not overridden it. Same
  // rule as softwareSettings and quoteSpecs.
  const [quoteArt, setQuoteArt] = useState(() => (persisted && persisted.quoteArt) || { overrides: {}, references: null });
  const [quoteTerms, setQuoteTerms] = useState(() => {
    const saved = persisted && persisted.quoteTerms;
    return (Array.isArray(saved) && saved.length) ? saved : [makeQuoteTermsSet()];
  });
  // LEON Sign lives at the TOP level, not on a project: an envelope routinely
  // covers a document that is not filed against one (an NDA, a supply
  // agreement), and one that is carries a `projectId` instead.
  const [signEnvelopes, setSignEnvelopes] = useState(() => (persisted && persisted.signEnvelopes) || []);
  const [signCounter, setSignCounter] = useState(() => (persisted && persisted.signCounter) || 1);
  setActiveQuoteSpecs(quoteSpecs);
  const [fenestrationLibrary, setFenestrationLibrary] = useState(() => (persisted && persisted.fenestrationLibrary) || null);
  useEffect(() => {
    if (typeof fenLibraryHydrate !== 'function' || typeof fenLibrarySubscribe !== 'function') return;
    fenLibraryHydrate(fenestrationLibrary);
    return fenLibrarySubscribe(() => setFenestrationLibrary(fenLibrary()));
    // Deliberately once: re-running it would re-hydrate from the state it just
    // wrote and fight the module for ownership.
  }, []);
  const [doorLibrary, setDoorLibrary] = useState(() => mergeNewDoorLibrary(persisted && persisted.doorLibrary));
  // Packages are the physical things on the rack. They persist like every other
  // top-level collection: useState, savePersistedState + its dependency array,
  // and ctx — miss any one and it silently never saves.
  const [warehousePackages, setWarehousePackages] = useState(() => (persisted && persisted.warehousePackages) || []);
  const [slabs, setSlabs] = useState(() => (persisted && persisted.slabs) || []);
  const [remnants, setRemnants] = useState(() => (persisted && persisted.remnants) || []);
  const [surfaceLibrary, setSurfaceLibrary] = useState(() => (persisted && persisted.surfaceLibrary) || { roomTypes: [], unitTypes: [], surfaceTypes: [], patterns: [] });
  // Same module-level registry as role permissions: instantiateStages and
  // startDateForJobsiteDate are pure and have no way to reach React state.
  // Re-pointed on every render, immediately after the state it reads.
  setActiveComplexityLevels(complexityLevels);
  const [interiorLeadTimeLibrary, setInteriorLeadTimeLibrary] = useState(() => (persisted && persisted.interiorLeadTimeLibrary) ? mergeNewInteriorLeadTimeEntries(persisted.interiorLeadTimeLibrary) : DEFAULT_INTERIOR_LEAD_TIME_LIBRARY);
  function addInteriorLeadTimeEntry(data) {
    setInteriorLeadTimeLibrary(prev => [...prev, makeInteriorLeadTimeEntry(data)]);
  }
  function updateInteriorLeadTimeEntry(entryId, fields) {
    setInteriorLeadTimeLibrary(prev => prev.map(e => e.id === entryId ? { ...e, ...fields } : e));
  }
  // Sets ONE stage's duration on one family's entry — the granularity the
  // library editor actually edits at.
  function setInteriorLeadTimeStageDays(entryId, stageKey, days) {
    const n = Number(days);
    setInteriorLeadTimeLibrary(prev => prev.map(e => e.id === entryId
      ? { ...e, stageDays: { ...e.stageDays, [stageKey]: n > 0 ? Math.round(n) : 0 } }
      : e));
  }
  function setInteriorLeadTimeEntryActive(entryId, active) {
    setInteriorLeadTimeLibrary(prev => prev.map(e => e.id === entryId ? { ...e, active } : e));
  }
  // Restores one family's entry to the STAGE_DEFS baseline durations.
  // The RUNNING ORDER for one scope-type variant of a family — the ordered
  // list of stage keys a new scope of that kind is built from. Saved only once
  // an admin edits it; resetting deletes the override so the family follows the
  // built-in template again, including any stage added to it later.
  // Complexity multipliers — editable and extendable, because how much longer
  // a high-end job takes is a judgement about a market, not a constant.
  function addComplexityLevel(data) { setComplexityLevels(prev => [...prev, makeComplexityLevel(data)]); }
  function updateComplexityLevel(id, fields) {
    const before = complexityLevels.find(l => l.id === id);
    setComplexityLevels(prev => prev.map(l => l.id === id ? { ...l, ...fields } : l));
    // A project stores its complexity as the NAME, so a rename has to carry
    // every project with it or those projects fall back to a 1x multiplier
    // without saying so.
    if (before && fields.name && fields.name !== before.name) {
      setProjects(prev => prev.map(p => p.complexity === before.name ? { ...p, complexity: fields.name } : p));
    }
  }
  function removeComplexityLevel(id) { setComplexityLevels(prev => prev.map(l => l.id === id ? { ...l, active: false } : l)); }

  function setScopeTemplateStages(entryId, variant, keys) {
    setInteriorLeadTimeLibrary(prev => prev.map(e => e.id === entryId
      ? { ...e, stageTemplates: { ...(e.stageTemplates || {}), [variant]: keys } } : e));
  }
  function resetScopeTemplate(entryId, variant) {
    setInteriorLeadTimeLibrary(prev => prev.map(e => {
      if (e.id !== entryId) return e;
      const next = { ...(e.stageTemplates || {}) };
      delete next[variant];
      return { ...e, stageTemplates: next };
    }));
  }
  function resetInteriorLeadTimeEntry(entryId) {
    setInteriorLeadTimeLibrary(prev => prev.map(e => e.id === entryId ? { ...e, stageDays: defaultInteriorStageDays() } : e));
  }

  // Stamp a numbered revision of the default schedule. The diff is computed
  // HERE, against the previous snapshot, and stored as sentences — recomputing
  // it later would go wrong the moment a stage or a family is renamed.
  function saveLeadTimeRevision(note) {
    const snap = leadTimeSnapshot(interiorLeadTimeLibrary, windowLeadTimeLibrary, complexityLevels);
    const last = leadTimeRevisions[0];
    const changes = leadTimeDiff(last ? last.snapshot : null, snap);
    const entry = {
      id: uid('ltrev'), n: (last ? last.n : 0) + 1, date: todayISO(),
      by: currentUserName, byRole: currentRole, note: note || '',
      changes, snapshot: snap,
    };
    // Capped: a snapshot is a few KB and the app has ~13 MB for everything.
    // Twenty revisions is a long history for a table that changes rarely.
    setLeadTimeRevisions(prev => [entry, ...prev].slice(0, 20));
    return entry;
  }
  // How far the live library has drifted from the last saved revision.
  function leadTimePendingChanges() {
    const snap = leadTimeSnapshot(interiorLeadTimeLibrary, windowLeadTimeLibrary, complexityLevels);
    const last = leadTimeRevisions[0];
    return leadTimeDiff(last ? last.snapshot : null, snap);
  }
  const [warehouses, setWarehouses] = useState(() => (persisted && persisted.warehouses) || SEED.warehouses);
  const [warehouseMaterials, setWarehouseMaterials] = useState(() => ((persisted && persisted.warehouseMaterials) || SEED.warehouseMaterials).map(normalizeWarehouseMaterial));
  const [inventoryTransactions, setInventoryTransactions] = useState(() => (persisted && persisted.inventoryTransactions) || SEED.inventoryTransactions);
  const [materialAllocations, setMaterialAllocations] = useState(() => ((persisted && persisted.materialAllocations) || SEED.materialAllocations).map(normalizeAllocation));
  const [warehouseReleases, setWarehouseReleases] = useState(() => (persisted && persisted.warehouseReleases) || SEED.warehouseReleases || []);
  const [packingLists, setPackingLists] = useState(() => (persisted && persisted.packingLists) || SEED.packingLists || []);
  const [personalItems, setPersonalItems] = useState(() => ((persisted && persisted.personalItems) || SEED.personalItems || []).map(normalizePersonalItem));
  const [logisticsClaims, setLogisticsClaims] = useState(() => (persisted && persisted.logisticsClaims) || SEED.logisticsClaims || []);
  const [trucks, setTrucks] = useState(() => (persisted && persisted.trucks) || SEED.trucks || []);
  function addTruck(data) { setTrucks(prev => [...prev, makeTruck(data)]); }
  function updateTruck(truckId, fields) { setTrucks(prev => prev.map(t => (t.id === truckId ? { ...t, ...fields } : t))); }
  // SUPERSEDED — the nine *Counter values below are no longer the source of a
  // document number. They are allocated by the database now
  // (leon_next_number), because a per-browser counter hands two people the same
  // PO number and tells neither. These remain only so persisted state keeps its
  // shape; nothing increments them, so do not read one expecting the truth.
  const [claimCounter, setClaimCounter] = useState(() => (persisted && persisted.claimCounter) || 1);
  async function reserveClaimNumber() {
    const n = await leonNextNumber('claimCounter');
    return `CLM-${new Date().getFullYear()}-${String(n).padStart(3, '0')}`;
  }
  const [tariffLibrary, setTariffLibrary] = useState(() => ((persisted && persisted.tariffLibrary) || SEED.tariffLibrary || []).map(normalizeTariffClassification));
  const [tariffLines, setTariffLines] = useState(() => ((persisted && persisted.tariffLines) || SEED.tariffLines || []).map(normalizeTariffLine));
  // Edits to the 658 imported US classifications land HERE, sparsely, keyed by
  // id — the supplier-catalog rule: static reference data stays in its own
  // <script> and out of localStorage, and a re-import can never wipe the team's
  // corrections. `tariffLibrary` above holds only classifications someone added
  // in the app.
  const [tariffOverrides, setTariffOverrides] = useState(() => (persisted && persisted.tariffOverrides) || {});
  // 51 rows, ~3KB — small enough to persist whole rather than as overrides, and
  // forward-merged so a rate someone corrected survives a later seed change.
  const [salesTaxRates, setSalesTaxRates] = useState(
    () => mergeNewSalesTaxRates((persisted && persisted.salesTaxRates) || []));
  function updateSalesTaxRate(code, fields) {
    setSalesTaxRates(prev => prev.map(r => (r.code === code ? { ...r, ...fields, edited: true } : r)));
  }
  function resetSalesTaxRate(code) {
    const seed = US_SALES_TAX_RATES.find(r => r.code === code);
    if (seed) setSalesTaxRates(prev => prev.map(r => (r.code === code ? { ...seed } : r)));
  }
  const [releaseCounter, setReleaseCounter] = useState(() => (persisted && persisted.releaseCounter) || 1);
  async function reserveReleaseNumber() {
    const n = await leonNextNumber('releaseCounter');
    return `RLS-${String(n).padStart(3, '0')}`;
  }
  const [packingListCounter, setPackingListCounter] = useState(() => (persisted && persisted.packingListCounter) || 1);
  async function reservePackingListNumber() {
    const n = await leonNextNumber('packingListCounter');
    return `PL-${String(n).padStart(3, '0')}`;
  }
  const [exportContainerCounter, setExportContainerCounter] = useState(() => (persisted && persisted.exportContainerCounter) || 1);
  async function reserveExportContainerNumber() {
    const n = await leonNextNumber('exportContainerCounter');
    return `LEX-${new Date().getFullYear()}-${String(n).padStart(3, '0')}`;
  }
  const [deliveryCounter, setDeliveryCounter] = useState(() => (persisted && persisted.deliveryCounter) || 1);
  async function reserveDeliveryNumber() {
    const n = await leonNextNumber('deliveryCounter');
    return `DEL-${new Date().getFullYear()}-${String(n).padStart(3, '0')}`;
  }
  const [subcontractors, setSubcontractors] = useState(() => ((persisted && persisted.subcontractors) || SEED.subcontractors).map(normalizeSubcontractor));
  const [poCounter, setPoCounter] = useState(() => (persisted && persisted.poCounter) || 10002);
  async function reservePoNumber() {
    const n = await leonNextNumber('poCounter');
    return `PO-${String(n).padStart(5, '0')}`;
  }
  const [estimateCounter, setEstimateCounter] = useState(() => (persisted && persisted.estimateCounter) || 1);
  async function reserveEstimateNumber() {
    const n = await leonNextNumber('estimateCounter');
    return `EST-${String(n).padStart(3, '0')}`;
  }
  const [piCounter, setPiCounter] = useState(() => (persisted && persisted.piCounter) || 1);
  async function reservePiNumber() {
    const n = await leonNextNumber('piCounter');
    return `PI-${String(n).padStart(3, '0')}`;
  }

  // Which team member is signed in drives the role — see the Login screen
  // below. authedUserId is null until a username/password pair matches.
  // Starts null and STAYS null until a real session is verified. It used to be
  // seeded straight from localStorage, which meant a hand-written entry in that
  // key was a way in — no password, no token, no server ever asked.
  const [authedUserId, setAuthedUserId] = useState(null);
  // True when this page was opened from a password-reset email. Read once on
  // mount, before anything can rewrite the address bar.
  const [recoveryMode, setRecoveryMode] = useState(
    () => (typeof leonAuthRecoveryPending === 'function') ? leonAuthRecoveryPending() : false);
  const [recoveryDone, setRecoveryDone] = useState(null);
  const [authChecking, setAuthChecking] = useState(true);
  const [currentUserId, setCurrentUserId] = useState(() => loadAuthSession() || teamDirectory[0].id);
  const [loginError, setLoginError] = useState('');
  // loadAuthSession() is read outside React, so clearing it needs a nudge.
  const [, forceRerender] = useState(0);
  // Sign-in history. Persisted like everything else, newest first.
  const [loginLog, setLoginLog] = useState(() => (persisted && persisted.loginLog) || []);
  function recordLogin(ev) { setLoginLog(prev => [makeLoginEvent(ev), ...prev].slice(0, LOGIN_LOG_LIMIT)); }

  // Restore a session by ASKING SUPABASE, not by trusting this browser. The
  // token is verified and refreshed server-side; a stale or forged local entry
  // resolves to nobody and the login screen is shown.
  //
  // The local branch exists only for running with no Supabase client at all
  // (offline, file://) while the data still lives in this browser. Once the
  // data is in Postgres it protects nothing and should go.
  useEffect(() => {
    let live = true;
    (async () => {
      try {
        // 'unavailable' (configured but the client did not load) must NOT fall
        // through to the local branch — that would restore a session no server
        // ever verified.
        const mode = (typeof leonAuthMode === 'function') ? leonAuthMode() : 'local';
        if (mode === 'supabase' || mode === 'unavailable') {
          const person = mode === 'supabase' ? await leonAuthCurrentPerson(teamDirectory) : null;
          if (!live) return;
          if (person) { setAuthedUserId(person.id); setCurrentUserId(person.id); saveAuthSession(person.id); }
          else { clearAuthSession(); }
        } else {
          const id = loadAuthSession();
          const who = id && teamDirectory.find(p => p.id === id && p.active);
          if (!live) return;
          if (who) { setAuthedUserId(who.id); setCurrentUserId(who.id); } else { clearAuthSession(); }
        }
      } catch (e) {
        if (live) clearAuthSession();
      } finally {
        if (live) setAuthChecking(false);
      }
    })();
    return () => { live = false; };
    // Once, on mount. Re-running it on every teamDirectory change would fight
    // with a sign-in that is already in flight.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // ASYNC now, because the password is verified by a server rather than compared
  // in this page. leonAuthSignIn resolves the username to an email, signs in
  // through Supabase, and hands back the LEON person record the role comes from.
  // It falls back to the old in-browser comparison only when the Supabase client
  // has not loaded at all (offline, file://) — never as a way past a failure.
  async function login(username, password) {
    const res = (typeof leonAuthSignIn === 'function')
      ? await leonAuthSignIn(teamDirectory, username, password)
      : { ok: false, error: 'Sign-in is unavailable — reload the page.' };
    if (!res.ok) {
      // Log the failure too — repeated failures on one username is the only
      // signal this app can give that someone is trying an account that is not
      // theirs. Never record the password that was tried.
      recordLogin({ username: username.trim(), outcome: 'Failed' });
      setLoginError(res.error);
      return false;
    }
    const match = res.person;
    recordLogin({ userId: match.id, name: match.name, username: match.username, role: match.securityRole, outcome: 'Signed in' });
    setLoginError('');
    setCurrentUserId(match.id);
    setAuthedUserId(match.id);
    saveAuthSession(match.id);
    // The Dashboard is the landing page — set explicitly rather than relying on
    // the initial useState default, since `view` isn't reset on logout and would
    // otherwise carry over whatever screen the previous session left it on in
    // this browser tab. A fresh sign-in always clears the remembered screen so
    // the previous session's position (or another user's, on a shared machine)
    // is never inherited. My To-Do is still a Calendar Hub subtab, and is also
    // a dashboard block.
    clearNavState();
    setView('dashboard');
    setNavMemo({ calendarSub: 'myToDo' });
    setSelectedProjectId(null);
    setSelectedAccountId(null);
    setSelectedVendorId(null);
    return true;
  }
  async function logout() {
    // End the Supabase session as well as the local one. Clearing only the local
    // flag would leave a valid token in this browser that a reload picks back up.
    if (typeof leonAuthSignOut === 'function') await leonAuthSignOut();
    const who = teamDirectory.find(p => p.id === authedUserId);
    if (who) recordLogin({ userId: who.id, name: who.name, username: who.username, role: who.securityRole, outcome: 'Signed out' });
    setAuthedUserId(null);
    setPendingProjectNav(null);
    setViewAsUserId(null);
    clearAuthSession();
    // Reset the screen state too, not just the stored copy — otherwise the
    // persist effect immediately re-saves the project that was open, leaving
    // the last user's position readable in this tab after they signed out.
    setView('dashboard');
    setSelectedProjectId(null);
    setSelectedAccountId(null);
    setSelectedVendorId(null);
    setNavMemo({});
    clearNavState();
  }

  // Where this browser tab was last looking. Read once on mount, so a refresh
  // comes back to the same screen; a fresh sign-in clears it (see login()).
  const initialNav = useMemo(() => loadNavState(), []);
  // A tab opened from "open in its own tab" carries the tool in its URL, and
  // that beats whatever this browser tab was last looking at. sessionStorage is
  // per-tab, so a new tab has none anyway — but a DUPLICATED tab inherits it,
  // and without this the copy would open on the old screen instead of the tool
  // the link asked for.
  const bootSoftware = useMemo(() => {
    try { return new URLSearchParams(window.location.search).get('software') || null; }
    catch (e) { return null; }
  }, []);
  const bootQuote = useMemo(() => {
    try {
      const v = new URLSearchParams(window.location.search).get('quote');
      if (!v) return null;
      const [projectId, qaId] = v.split('~');
      return projectId && qaId ? { projectId, qaId } : null;
    } catch (e) { return null; }
  }, []);
  const [view, setView] = useState(
    bootSoftware ? 'leonStudio' : bootQuote ? 'project' : (initialNav.view || 'dashboard')); // dashboard | calendar | accounts | accountDetail | project | admin | library | vendors | vendorDetail | users
  const [selectedProjectId, setSelectedProjectId] = useState((bootQuote && bootQuote.projectId) || initialNav.projectId || null);
  const [selectedAccountId, setSelectedAccountId] = useState(initialNav.accountId || null);
  const [selectedVendorId, setSelectedVendorId] = useState(initialNav.vendorId || null);
  const [selectedVendorType, setSelectedVendorType] = useState(initialNav.vendorType || 'vendor'); // 'vendor' | 'forwarder'
  // Sub-screen positions (which project tab, which Calendar Hub subtab) that
  // live in child components' own state — kept here only so they can be
  // restored after a refresh. Children read `navMemo` for their initial value
  // and call `rememberNav` when the user moves.
  const [navMemo, setNavMemo] = useState(() => initialNav);
  const rememberNav = useCallback(patch => setNavMemo(prev => ({ ...prev, ...patch })), []);
  const [dashboardFilter, setDashboardFilter] = useState('All');
  const [search, setSearch] = useState('');
  // Set by ctx.goProjectTab (Reports drill-down, global search) so
  // ProjectDetail can open directly to a specific tab/subtab in one hop.
  const [pendingProjectNav, setPendingProjectNav] = useState(null);
  // Same idea for the Reports hub — set by ctx.goReport (Logistics Dashboard
  // KPI cards) so ReportsHubTab can open straight into one report,
  // optionally pre-filtered, instead of landing on the category list.
  const [pendingReportNav, setPendingReportNav] = useState(null);
  // Same idea for the Trade Compliance & Tariffs hub — set by ctx.
  // goTradeCompliance(subtab) so a deep link (Export container, Logistics
  // report) can land straight on Tariff Lines/Exposure instead of the
  // module's own Dashboard.
  const [pendingTradeComplianceNav, setPendingTradeComplianceNav] = useState(null);
  // Which section of each hub is open, keyed by hub. It lives here rather than
  // inside each hub because the NAV MENUS list the sections directly and have
  // to both drive and highlight them — a hub owning its own tab state could do
  // neither. One map rather than a state per hub, so adding a hub to the nav is
  // a row in HUB_NAV (Header) and one call to useHubSection in the hub itself.
  const [hubSections, setHubSections] = useState(() => (bootSoftware ? { leonStudio: bootSoftware } : {}));
  // A ?quote= link lands on the job's Sales Hub with the draft already open.
  const [bootQuoteId, setBootQuoteId] = useState(() => (bootQuote ? bootQuote.qaId : null));
  const setHubSection = useCallback((hub, key) => {
    setHubSections(prev => (prev[hub] === key ? prev : { ...prev, [hub]: key }));
  }, []);

  // Remember the current screen for a refresh. Deliberately separate from
  // savePersistedState (that's the app's DATA; this is just where you were).
  useEffect(() => {
    saveNavState({ ...navMemo, view, projectId: selectedProjectId, accountId: selectedAccountId, vendorId: selectedVendorId, vendorType: selectedVendorType });
  }, [navMemo, view, selectedProjectId, selectedAccountId, selectedVendorId, selectedVendorType]);

  // A restored screen can point at something that no longer exists (a project
  // deleted from another tab, or a record the user can no longer see) — fall
  // back to the dashboard rather than rendering an empty detail page.
  useEffect(() => {
    if (view === 'project' && selectedProjectId && !projects.some(p => p.id === selectedProjectId)) setView('dashboard');
    if (view === 'accountDetail' && selectedAccountId && !accounts.some(a => a.id === selectedAccountId)) setView('accounts');
  }, [view, selectedProjectId, selectedAccountId, projects, accounts]);

  useEffect(() => {
    savePersistedState({ notifications, notificationPrefs, emailOutbox, shares, loginLog, accounts, projects, scopeLibrary, teamDirectory, documentLibrary, companyProfile, vendors, freightForwarders, poCounter, estimateCounter, piCounter, materialLibrary, scopeDocuments, applianceLibrary, fixtureLibrary, windowLeadTimeLibrary, interiorLeadTimeLibrary, complexityLevels, holidays, rolePermissions, permissionLog, supplierFinishOverrides, supplierVendorLinks, importedFinishes: importedFinishes_, cashEntries, cashSettings, creditCards, subcontractors, warehouses, warehouseMaterials, inventoryTransactions, materialAllocations, warehouseReleases, packingLists, releaseCounter, packingListCounter, personalItems, exportContainers, exportContainerCounter, deliveryCounter, logisticsClaims, claimCounter, tariffLibrary, tariffOverrides, salesTaxRates, tariffLines, trucks, doorLibrary, warehousePackages, slabs, remnants, surfaceLibrary, officeDocs, ctPriceLists, fenestrationLibrary, softwareSettings, quoteRecipes, quoteSpecs, quoteSpecImages, quoteTerms, quoteArt, signEnvelopes, signCounter, leadTimeRevisions, scratchProjects });
  }, [notifications, notificationPrefs, emailOutbox, shares, loginLog, accounts, projects, scopeLibrary, teamDirectory, documentLibrary, companyProfile, vendors, freightForwarders, poCounter, estimateCounter, piCounter, materialLibrary, scopeDocuments, applianceLibrary, fixtureLibrary, windowLeadTimeLibrary, interiorLeadTimeLibrary, complexityLevels, holidays, rolePermissions, permissionLog, supplierFinishOverrides, supplierVendorLinks, importedFinishes_, cashEntries, cashSettings, creditCards, subcontractors, warehouses, warehouseMaterials, inventoryTransactions, materialAllocations, warehouseReleases, packingLists, releaseCounter, packingListCounter, personalItems, exportContainers, exportContainerCounter, deliveryCounter, logisticsClaims, claimCounter, tariffLibrary, tariffOverrides, salesTaxRates, tariffLines, trucks, doorLibrary, warehousePackages, slabs, remnants, surfaceLibrary, officeDocs, ctPriceLists, fenestrationLibrary, softwareSettings, quoteRecipes, quoteSpecs, quoteSpecImages, quoteTerms, quoteArt, signEnvelopes, signCounter, leadTimeRevisions, scratchProjects]);

  // -------------------------------------------------------------------------
  // Shared reference data (phase 1)
  //
  // Thirty collections that every person must see identically — the scope
  // library, lead times, the role permission matrix, the roster, quote
  // settings. Until now each browser held its own copy, which for the matrix
  // in particular is not "not shared" but wrong: setting someone's permissions
  // changed them in one browser and nowhere else.
  //
  // Local first, server reconciles. The app has already rendered from
  // localStorage by the time any of this runs, so a slow or absent network
  // costs nothing but freshness.
  // -------------------------------------------------------------------------
  const referenceSetters = {
    scopeLibrary: setScopeLibrary, interiorLeadTimeLibrary: setInteriorLeadTimeLibrary,
    windowLeadTimeLibrary: setWindowLeadTimeLibrary, complexityLevels: setComplexityLevels,
    rolePermissions: setRolePermissions, permissionLog: setPermissionLog,
    companyProfile: setCompanyProfile, teamDirectory: setTeamDirectory,
    quoteRecipes: setQuoteRecipes, quoteSpecs: setQuoteSpecs,
    quoteSpecImages: setQuoteSpecImages, quoteTerms: setQuoteTerms, quoteArt: setQuoteArt,
    salesTaxRates: setSalesTaxRates, holidays: setHolidays, tariffOverrides: setTariffOverrides,
    doorLibrary: setDoorLibrary, surfaceLibrary: setSurfaceLibrary,
    fenestrationLibrary: setFenestrationLibrary, softwareSettings: setSoftwareSettings,
    materialLibrary: setMaterialLibrary, scopeDocuments: setScopeDocuments,
    applianceLibrary: setApplianceLibrary, fixtureLibrary: setFixtureLibrary,
    documentLibrary: setDocumentLibrary, supplierFinishOverrides: setSupplierFinishOverrides,
    supplierVendorLinks: setSupplierVendorLinks, importedFinishes: setImportedFinishes,
    leadTimeRevisions: setLeadTimeRevisions, ctPriceLists: setCtPriceLists,
    cashSettings: setCashSettings,
  };
  const referenceValues = {
    scopeLibrary, interiorLeadTimeLibrary, windowLeadTimeLibrary, complexityLevels,
    rolePermissions, permissionLog, companyProfile, teamDirectory, quoteRecipes, quoteSpecs,
    quoteSpecImages, quoteTerms, quoteArt, salesTaxRates, holidays, tariffOverrides,
    doorLibrary, surfaceLibrary, fenestrationLibrary, softwareSettings, materialLibrary,
    scopeDocuments, applianceLibrary, fixtureLibrary, documentLibrary, supplierFinishOverrides,
    supplierVendorLinks, importedFinishes: importedFinishes_, leadTimeRevisions, ctPriceLists,
    cashSettings,
  };
  // Held in a ref as well, because the push effect must read the CURRENT values
  // without listing all thirty in its dependency array — which would make it
  // re-run on every keystroke anywhere in the app.
  const referenceValuesRef = useRef(referenceValues);
  referenceValuesRef.current = referenceValues;

  const [syncState, setSyncState] = useState({ status: 'idle', error: '', conflicts: [] });

  // A failed number allocation must be SEEN. Most reserve call sites are
  // fire-and-forget — nothing awaits their result — so a rejection would
  // otherwise reach only the console, and the person would be left wondering
  // why pressing Create did nothing at all. One listener catches every site.
  const [opError, setOpError] = useState('');
  useEffect(() => {
    function onReject(e) {
      const msg = (e && e.reason && e.reason.message) || '';
      if (/document number|allocate|Not signed in|reach the server/i.test(msg)) {
        setOpError(msg);
        e.preventDefault();   // it is handled — here, visibly
      }
    }
    window.addEventListener('unhandledrejection', onReject);
    return () => window.removeEventListener('unhandledrejection', onReject);
  }, []);

  // PULL, once signed in. Applies only what genuinely differs, so a person
  // whose copy already matches sees no re-render at all.
  useEffect(() => {
    if (!authedUserId || typeof leonSyncPullReference !== 'function') return;
    let cancelled = false;
    (async () => {
      setSyncState(s => ({ ...s, status: 'pulling' }));
      const res = await leonSyncPullReference();
      if (cancelled) return;
      if (!res.ok) {
        // A missing table is the expected state mid-rollout, not a fault, and
        // the app carries on entirely on its local copy either way.
        setSyncState({ status: res.missing ? 'not-set-up' : 'offline', error: res.error, conflicts: [] });
        return;
      }
      let applied = 0;
      Object.keys(res.data).forEach(key => {
        const setter = referenceSetters[key];
        if (!setter) return;
        if (leonSyncSame(referenceValuesRef.current[key], res.data[key])) return;
        setter(res.data[key]);
        applied++;
      });
      setSyncState({ status: 'synced', error: '', conflicts: [], applied,
                     shared: Object.keys(res.data).length });
    })();
    return () => { cancelled = true; };
  }, [authedUserId]);

  // PUSH what changed. Debounced, because a slider or a typed field produces a
  // burst of state updates and each one must not become its own round trip.
  //
  // Only keys ALREADY shared are written — see leonSyncPushReference. Seeding
  // the first copy is a deliberate act, never a side effect of a save.
  const pushedRef = useRef(null);
  useEffect(() => {
    if (!authedUserId || syncState.status !== 'synced') return;
    if (typeof leonSyncPushReference !== 'function') return;
    const t = setTimeout(async () => {
      const now = referenceValuesRef.current;
      const last = pushedRef.current || {};
      const conflicts = [];
      for (const key of LEON_REFERENCE_KEYS) {
        if (!(key in now)) continue;
        if (key in last && leonSyncSame(last[key], now[key])) continue;
        if (!(key in last)) { last[key] = now[key]; continue; } // first pass: record, do not write
        const res = await leonSyncPushReference(key, now[key]);
        if (res.ok) last[key] = now[key];
        else if (res.conflict) conflicts.push(key);
      }
      pushedRef.current = last;
      if (conflicts.length) setSyncState(s => ({ ...s, conflicts }));
    }, 1200);
    return () => clearTimeout(t);
  }, [authedUserId, syncState.status, scopeLibrary, interiorLeadTimeLibrary,
      windowLeadTimeLibrary, complexityLevels, rolePermissions, permissionLog, companyProfile,
      teamDirectory, quoteRecipes, quoteSpecs, quoteSpecImages, quoteTerms, quoteArt,
      salesTaxRates, holidays, tariffOverrides, doorLibrary, surfaceLibrary, fenestrationLibrary,
      softwareSettings, materialLibrary, scopeDocuments, applianceLibrary, fixtureLibrary,
      documentLibrary, supplierFinishOverrides, supplierVendorLinks, importedFinishes_,
      leadTimeRevisions, ctPriceLists, cashSettings]);

  // -------------------------------------------------------------------------
  // PHASE 2 WAVE 1 — the operational records.
  //
  // Reference data above is one row per collection. These are one row per
  // RECORD, because they are edited all day by everybody and two people
  // working on two different vendors must not be able to lose each other's
  // work.
  //
  // Wave 1 is deliberately the collections that reference NOTHING, so the trap
  // that wiped ten vendors in phase 1 — sharing something before the records
  // it points at — cannot apply. projects come next, and separately.
  // -------------------------------------------------------------------------
  const recordSetters = {
    vendors: setVendors, accounts: setAccounts, warehouses: setWarehouses,
    freightForwarders: setFreightForwarders, trucks: setTrucks,
    // wave 2
    warehouseMaterials: setWarehouseMaterials, projects: setProjects,
    subcontractors: setSubcontractors,
    // wave 3 — the logistics records
    inventoryTransactions: setInventoryTransactions,
    materialAllocations: setMaterialAllocations,
    warehouseReleases: setWarehouseReleases, packingLists: setPackingLists,
    warehousePackages: setWarehousePackages, exportContainers: setExportContainers,
    logisticsClaims: setLogisticsClaims,
    // wave 4
    notifications: setNotifications, emailOutbox: setEmailOutbox, shares: setShares,
    officeDocs: setOfficeDocs, signEnvelopes: setSignEnvelopes,
    cashEntries: setCashEntries, creditCards: setCreditCards,
    tariffLibrary: setTariffLibrary, tariffLines: setTariffLines,
    slabs: setSlabs, remnants: setRemnants, loginLog: setLoginLog,
    scratchProjects: setScratchProjects, personalItems: setPersonalItems,
  };
  const recordValues = {
    vendors, accounts, warehouses, freightForwarders, trucks,
    warehouseMaterials, projects, subcontractors,
    inventoryTransactions, materialAllocations, warehouseReleases, packingLists,
    warehousePackages, exportContainers, logisticsClaims,
    notifications, emailOutbox, shares, officeDocs, signEnvelopes,
    cashEntries, creditCards, tariffLibrary, tariffLines, slabs, remnants,
    loginLog, scratchProjects, personalItems,
  };

  // A record read back from the server gets the SAME normalisation a record
  // read back from localStorage gets, and for the same reason: `normalizeX`
  // is where new fields are backfilled onto records saved before those fields
  // existed. Skipping it on the pull would mean a project that loads fine from
  // this browser's own storage blanks the app when it arrives from Postgres.
  //
  // `normalizeProject` in particular does NOT backfill eleven of the arrays a
  // project needs, so a record that never went through it can crash a render.
  const recordNormalizers = {
    projects: normalizeProject, subcontractors: normalizeSubcontractor,
    warehouseMaterials: normalizeWarehouseMaterial, vendors: normalizeVendor,
    accounts: normalizeAccount, freightForwarders: normalizeVendor,
    materialAllocations: normalizeAllocation, exportContainers: normalizeExportContainer,
  };
  const normalizeRecords = (collection, list) => {
    const fn = recordNormalizers[collection];
    return fn ? (list || []).map(fn) : (list || []);
  };
  const recordValuesRef = useRef(recordValues);
  recordValuesRef.current = recordValues;

  // The last copy known to be on the server, per collection. The push diffs
  // against it, so editing one vendor sends one vendor.
  const recordPushedRef = useRef({});

  // PULL the shared records once signed in. Applies only what genuinely
  // differs, so a browser already in step re-renders nothing.
  useEffect(() => {
    if (!authedUserId || typeof leonSyncPullRecords !== 'function') return;
    let cancelled = false;
    (async () => {
      const res = await leonSyncPullRecords();
      if (cancelled || !res.ok) return;
      Object.keys(res.published || {}).forEach(collection => {
        const setter = recordSetters[collection];
        if (!setter) return;
        const incoming = normalizeRecords(collection, res.data[collection]);
        if (leonSyncSame(recordValuesRef.current[collection], incoming)) return;
        setter(incoming);
      });
      // Seed the push baseline from what the server actually holds, so the
      // first save after sign-in pushes the one record that changed rather
      // than re-writing every vendor.
      //
      // Every PUBLISHED collection gets a baseline, including one the server
      // holds no rows for — `[]` is a real answer there, and leaving it absent
      // would make the push treat the first genuine edit as a first pass and
      // skip it.
      //
      // The baseline is the NORMALISED form, matching what local state now
      // holds. Recording the raw server copy instead would make normalisation
      // itself look like an edit, and all 26 browsers would push every record
      // they loaded — which is both pointless churn and 26 chances to collide.
      const baseline = {};
      Object.keys(res.published || {}).forEach(c => {
        baseline[c] = normalizeRecords(c, res.data[c]);
      });
      recordPushedRef.current = baseline;
    })();
    return () => { cancelled = true; };
  }, [authedUserId]);

  // PUSH what changed, debounced, in the background.
  //
  // The client's own choice: the app stays instant and reconciles afterwards,
  // rather than making anyone wait on the network for a keystroke. A conflict
  // therefore arrives AFTER the fact — which is why it is reported loudly
  // instead of being silently merged.
  useEffect(() => {
    if (!authedUserId || typeof leonSyncPushCollection !== 'function') return;
    if (!leonSyncRecordsReady || !leonSyncRecordsReady()) return;
    const t = setTimeout(async () => {
      const now = recordValuesRef.current;
      const conflicts = [];
      for (const collection of LEON_RECORD_COLLECTIONS) {
        if (!leonSyncRecordPublished(collection)) continue;
        const prev = recordPushedRef.current[collection];
        if (prev === undefined) { recordPushedRef.current[collection] = now[collection]; continue; }
        if (leonSyncSame(prev, now[collection])) continue;
        const res = await leonSyncPushCollection(collection, now[collection], prev);
        if (res.ok) recordPushedRef.current[collection] = now[collection];
        (res.conflicts || []).forEach(id => conflicts.push(collection + ':' + id));
      }
      if (conflicts.length) setSyncState(s => ({ ...s, recordConflicts: conflicts }));
    }, 1200);
    return () => clearTimeout(t);
  }, [authedUserId, vendors, accounts, warehouses, freightForwarders, trucks,
      warehouseMaterials, projects, subcontractors,
      inventoryTransactions, materialAllocations, warehouseReleases, packingLists,
      warehousePackages, exportContainers, logisticsClaims,
      notifications, emailOutbox, shares, officeDocs, signEnvelopes,
      cashEntries, creditCards, tariffLibrary, tariffLines, slabs, remnants,
      loginLog, scratchProjects, personalItems]);

  // A conflict is reported by collection:id. A person does not know what
  // `vendors:v-12` is, so it is resolved to the record's own name — and it is
  // resolved from the LOCAL copy, which is the one that still holds the edit
  // that was refused.
  const syncConflictLabels = useMemo(() => {
    const out = [];
    const seen = {};
    ((syncState.conflicts || []).concat(syncState.recordConflicts || [])).forEach(entry => {
      if (seen[entry]) return;
      seen[entry] = true;
      const i = String(entry).indexOf(':');
      if (i === -1) { out.push(entry); return; }               // a reference collection
      const collection = entry.slice(0, i), id = entry.slice(i + 1);
      const rec = (recordValuesRef.current[collection] || []).find(r => r && r.id === id);
      out.push((rec && (rec.name || rec.companyName || rec.label)) || `${collection} ${id}`);
    });
    return out;
  }, [syncState.conflicts, syncState.recordConflicts]);

  // The one-time publish. Exposed on ctx rather than run automatically: it
  // decides WHICH browser's copy becomes everyone's, and that is a decision.
  //
  // Records go first and reference second, and that order is load-bearing:
  // supplierVendorLinks maps a catalog to a VENDOR ID, and publishing it
  // before the vendors exist is exactly what wiped ten of them last time.
  // leonSyncPublishAll refuses to run the second half if the first fails.
  async function publishReferenceData() {
    if (typeof leonSyncPublishAll !== 'function') return { ok: false, error: 'Sync unavailable.' };
    const res = await leonSyncPublishAll(recordValuesRef.current, referenceValuesRef.current);
    const seeded = ((res.reference && res.reference.seeded) || []).length
                 + ((res.records && res.records.seeded) || []).length;
    if (res.ok || seeded) setSyncState(s => ({ ...s, status: 'synced' }));
    return res;
  }

  // View As (Admin/Accounting only) — lets a real Admin/Accounting login see
  // the app exactly as another person would, permissions and all. Session-
  // only (never persisted, resets on logout) so it can never accidentally
  // linger. Every currentUser-derived value below flips over to the viewed
  // person while active, including who actions get attributed to — this is
  // a transparent, banner-announced "acting as," not a silent read-only
  // preview, since locking every edit control app-wide isn't practical here.
  const realCurrentUser = useMemo(() => teamDirectory.find(p => p.id === currentUserId) || teamDirectory[0], [teamDirectory, currentUserId]);
  const canViewAsOthers = !!(realCurrentUser && ['Admin', 'Accounting'].includes(realCurrentUser.securityRole));
  const [viewAsUserId, setViewAsUserId] = useState(null);
  const isViewingAs = !!(canViewAsOthers && viewAsUserId && realCurrentUser && viewAsUserId !== realCurrentUser.id);
  const currentUser = useMemo(() => {
    if (isViewingAs) return teamDirectory.find(p => p.id === viewAsUserId) || realCurrentUser;
    return realCurrentUser;
  }, [teamDirectory, isViewingAs, viewAsUserId, realCurrentUser]);

  // What day it is, from the signed-in person's office — at 03:52 in Ho Chi
  // Minh City it is already tomorrow while Boston is still on yesterday, and a
  // shared calendar that computed one answer for everyone would put a
  // colleague's work on the wrong day. Placed immediately after currentUser is
  // defined, NOT up with the other registries: reading it earlier hits the
  // temporal dead zone and blanks the app, which is exactly how the complexity
  // registry broke once before.
  setActiveTimeZone(officeTimeZone(currentUser && currentUser.officeLocation));
  // WHO THE APP THINKS YOU ARE right now. `currentUserId` above is the real
  // signed-in session and never changes; this follows View As. Anything that
  // asks "is this mine?" must use THIS one — ctx.currentUserId is this value,
  // so a component reading ctx is already correct; only code using the raw
  // closure variable can get it wrong.
  const effectiveUserId = currentUser ? currentUser.id : null;
  const currentUserName = currentUser ? currentUser.name : 'Unknown';
  const currentRole = currentUser ? currentUser.securityRole : 'Admin';
  const canSeeFin = canSeeFinancials(currentRole);
  const canSeeAcctHub = canSeeAccountingHub(currentRole);
  const canEdit = useCallback((moduleKey) => canEditModule(currentRole, moduleKey, currentUser), [currentRole, currentUser]);
  const canView = useCallback((moduleKey) => canViewModule(currentRole, moduleKey, currentUser), [currentRole, currentUser]);

  // ---- Department scope (Windows / Interiors) -----------------------------
  // The data-scope axis that sits alongside the role/capability axis above.
  // `myDepartments` is what this person is allowed to see at all;
  // `activeDepartment` is what they're currently LOOKING at — a filter they
  // choose, not a permission. Someone covering only one department has no
  // choice to make, so the selector collapses to a static label for them and
  // activeDepartment is pinned to their one department.
  const myDepartments = useMemo(() => selectableDepartments(currentUser), [currentUser]);
  const canChooseDepartment = myDepartments.length > 1;
  const [departmentChoice, setDepartmentChoice] = useState(ALL_DEPARTMENTS);
  const activeDepartment = canChooseDepartment
    ? (departmentChoice === ALL_DEPARTMENTS || myDepartments.includes(departmentChoice) ? departmentChoice : ALL_DEPARTMENTS)
    : myDepartments[0];
  // Reset the choice whenever the effective person changes (login, or an
  // Admin using View As) — otherwise a stale choice from the previous
  // person's departments leaks into the new one's view.
  useEffect(() => { setDepartmentChoice(ALL_DEPARTMENTS); }, [currentUser && currentUser.id]);
  // Convenience wrappers so screens don't each have to thread the library
  // and the active department through by hand.
  const deptScopes = useCallback((project) => scopesInDepartment(project, activeDepartment, scopeLibrary), [activeDepartment, scopeLibrary]);
  const deptProjects = useCallback((list) => (list || []).filter(p => projectInDepartment(p, activeDepartment, scopeLibrary)), [activeDepartment, scopeLibrary]);
  const deptRecords = useCallback((records, project, key) => recordsInDepartment(records, project, activeDepartment, scopeLibrary, key), [activeDepartment, scopeLibrary]);

  function updateProject(id, mutator) {
    // A scratch project is a project in every way a module cares about, so the
    // one mutator every module already uses has to find it. Without this a tool
    // used before a job existed would report success and write nothing.
    const apply = p => {
      const draft = cloneDeep(p);
      mutator(draft);
      draft.health = computeProjectHealth(draft);
      return draft;
    };
    if (String(id).startsWith('scratch-')) {
      setScratchProjects(prev => prev.map(p => (p.id === id ? apply(p) : p)));
      return;
    }
    setProjects(prev => prev.map(p => (p.id === id ? apply(p) : p)));
  }
  // The signed-in person's own scratch project, created the first time it is
  // needed. One per person so two people are not measuring into each other's.
  // Send a table into LEON Sheets as a real document, not a CSV download.
  // One exporter for every screen that produces a table — a quote analysis, a bill
  // of quantities, anything later — so the sheet always arrives the same shape:
  // a header row, the rows beneath it, sensible column widths and a frozen
  // header. `rows` is an array of arrays; a cell may be a string, a number, or
  // { v, bold, fmt } when a line needs to stand out or carry a money format.
  function exportToSheet(spec) {
    const rows = spec.rows || [];
    const cells = {};
    rows.forEach((row, r) => (row || []).forEach((cell, c) => {
      if (cell === null || cell === undefined || cell === '') return;
      const o = (typeof cell === 'object') ? cell : { v: cell };
      const st = {};
      if (o.bold) st.b = true;
      if (o.fmt) st.nfp = o.fmt;
      cells[r + ',' + c] = st.b || st.nfp ? { v: o.v, s: st } : { v: o.v };
    }));
    const body = makeSheetBody();
    const ws = body.sheets[0];
    ws.name = (spec.sheetName || 'Sheet 1').slice(0, 28);
    ws.cells = cells;
    // Widths in characters, the way Excel measures them.
    (spec.colWidths || []).forEach((w, i) => { if (w) ws.cols[i] = { wch: w }; });
    // The header row stays put while the bill is scrolled.
    ws.frozen = { r: spec.freezeRows === undefined ? 1 : spec.freezeRows, c: 0 };

    const doc = makeOfficeDocument({
      name: spec.name || 'Export', app: 'sheet',
      folder: spec.projectId ? 'Project' : 'Company',
      projectId: spec.projectId || null, body,
    }, currentUserName);
    doc.activity = [{ id: uid('act'), date: todayISO(), by: currentUserName,
      text: 'Created by export from ' + (spec.source || 'the Hub') + '.' }];
    setOfficeDocs(prev => [doc].concat(prev || []));
    return doc;
  }

  // What is actually sitting in an unassigned workspace. Detected rather than
  // listed: any array the tools have written to. A hardcoded list would go
  // stale the first time a software stores something new.
  function scratchContents(sp) {
    if (!sp) return [];
    const skip = { changeLog: 1, contacts: 1, teams: 1, companyDepartment: 1 };
    return Object.keys(sp).filter(k => !skip[k] && Array.isArray(sp[k]) && sp[k].length)
      .map(k => ({ key: k, label: SCRATCH_LABELS[k] || k, count: sp[k].length }));
  }
  // Move chosen work onto a real job. It is a MOVE, not a copy: leaving a
  // duplicate behind is how two versions of the same take-off end up on two
  // different screens. The change is logged on the receiving job, because that
  // is where someone will later ask where this came from.
  function moveScratchWork(scratchId, targetProjectId, keys) {
    const sp = scratchProjects.find(p => p.id === scratchId);
    if (!sp || !targetProjectId || !keys || !keys.length) return 0;
    let moved = 0;
    const payload = {};
    keys.forEach(k => { if (Array.isArray(sp[k]) && sp[k].length) { payload[k] = cloneDeep(sp[k]); moved += sp[k].length; } });
    updateProject(targetProjectId, draft => {
      Object.keys(payload).forEach(k => {
        draft[k] = (draft[k] || []).concat(payload[k]);
      });
      logAction(draft, `Moved ${moved} record${moved === 1 ? '' : 's'} here from an unassigned workspace (`
        + Object.keys(payload).map(k => SCRATCH_LABELS[k] || k).join(', ') + ').');
    });
    setScratchProjects(prev => prev.map(p => {
      if (p.id !== scratchId) return p;
      const next = { ...p };
      Object.keys(payload).forEach(k => { next[k] = []; });
      return next;
    }));
    return moved;
  }

  function myScratchProject() {
    const mine = scratchProjects.find(p => p.ownerId === effectiveUserId);
    if (mine) return mine;
    const made = makeScratchProject(effectiveUserId, currentUserName);
    setScratchProjects(prev => prev.concat([made]));
    return made;
  }
  // Every project a TOOL may work on: the real jobs this person can see, plus
  // their own scratch. Business screens keep using ctx.projects and so can
  // never see it.
  function toolProjects() {
    const mine = scratchProjects.filter(p => p.ownerId === effectiveUserId);
    return projects.concat(mine);
  }
  function logAction(draft, action) {
    draft.changeLog = draft.changeLog || [];
    draft.changeLog.unshift({ id: uid('log'), date: todayISO(), role: currentRole, user: currentUserName, action });
  }

  // ---- Accounts ----
  function addAccount(data) {
    const acct = { id: uid('acct'), attachments: [], activityLog: [], contacts: [], accountType: 'Other', region: ACCOUNT_REGIONS[0], logoUrl: null, contactMobile: '', website: '', notes: '', createdDate: todayISO(), ...data };
    setAccounts(prev => [...prev, acct]);
    return acct;
  }
  function updateAccount(accountId, fields) {
    setAccounts(prev => prev.map(a => a.id === accountId ? { ...a, ...fields } : a));
  }
  // Client Portal login — same pattern as addSubcontractor's inline portal
  // login: a real teamDirectory entry (the login system's source of truth,
  // since login() only ever checks teamDirectory) with securityRole
  // 'Client' and an accountId link back to the account, instead of a
  // separate parallel login collection. Once created it's manageable like
  // any other login from Users (Set Password, Deactivate). Restricting a
  // client to only their own jobs falls straight out of this link —
  // ClientPortal filters projects by accountId, nothing else needed.
  function addClientPortalLogin(accountId, data) {
    const account = accounts.find(a => a.id === accountId);
    if (!account) return;
    setTeamDirectory(prev => [...prev, {
      id: uid('person'), name: data.name || account.contactName, roles: [], securityRole: 'Client',
      email: data.email || account.email, username: data.username.trim().toLowerCase(), password: data.password || '',
      active: true, accountId,
    }]);
  }
  // Shared attachment CRUD for the global record-access standard — Accounts,
  // Vendors, Freight Forwarders, and Subcontractors all get the same
  // {id,name,url,uploadedBy,uploadedDate} attachment shape and an append-only
  // activityLog entry, without duplicating this logic per entity type.
  function addAttachmentTo(setState, id, data) {
    setState(prev => prev.map(x => x.id !== id ? x : {
      ...x,
      attachments: [...(x.attachments || []), { id: uid('att'), name: data.name, url: data.url, uploadedBy: currentUserName, uploadedDate: todayISO() }],
      activityLog: [...(x.activityLog || []), makeActivityEntry({ user: currentUserName, action: `Attached ${data.name}` })],
    }));
  }
  function removeAttachmentFrom(setState, id, attId) {
    setState(prev => prev.map(x => x.id !== id ? x : {
      ...x,
      attachments: (x.attachments || []).filter(a => a.id !== attId),
      activityLog: [...(x.activityLog || []), makeActivityEntry({ user: currentUserName, action: 'Removed attachment' })],
    }));
  }
  // Catalog: anyone can add; once added, only Admin can remove (enforced
  // here too, not just by hiding the button, since ctx functions are the
  // real boundary). Price List uses the same add/remove shape but has no
  // extra delete restriction — its protection is who can even VIEW it
  // (canSeeVendorPricing, checked in the UI).
  function addCatalogEntryTo(setState, id, data) {
    setState(prev => prev.map(x => x.id !== id ? x : { ...x, catalog: [...(x.catalog || []), makeCatalogEntry(data, currentUserName)] }));
  }
  function removeCatalogEntryFrom(setState, id, catalogId) {
    if (currentRole !== 'Admin') return;
    setState(prev => prev.map(x => x.id !== id ? x : { ...x, catalog: (x.catalog || []).filter(c => c.id !== catalogId) }));
  }
  function addPriceListEntryTo(setState, id, data) {
    setState(prev => prev.map(x => x.id !== id ? x : { ...x, priceList: [...(x.priceList || []), makePriceListEntry(data, currentUserName)] }));
  }
  function removePriceListEntryFrom(setState, id, priceId) {
    setState(prev => prev.map(x => x.id !== id ? x : { ...x, priceList: (x.priceList || []).filter(p => p.id !== priceId) }));
  }
  const addVendorCatalogEntry = (id, data) => addCatalogEntryTo(setVendors, id, data);
  const removeVendorCatalogEntry = (id, catalogId) => removeCatalogEntryFrom(setVendors, id, catalogId);
  const addFreightForwarderCatalogEntry = (id, data) => addCatalogEntryTo(setFreightForwarders, id, data);
  const removeFreightForwarderCatalogEntry = (id, catalogId) => removeCatalogEntryFrom(setFreightForwarders, id, catalogId);
  const addVendorPriceListEntry = (id, data) => addPriceListEntryTo(setVendors, id, data);
  const removeVendorPriceListEntry = (id, priceId) => removePriceListEntryFrom(setVendors, id, priceId);
  const addFreightForwarderPriceListEntry = (id, data) => addPriceListEntryTo(setFreightForwarders, id, data);
  const removeFreightForwarderPriceListEntry = (id, priceId) => removePriceListEntryFrom(setFreightForwarders, id, priceId);
  const addAccountAttachment = (id, data) => addAttachmentTo(setAccounts, id, data);
  const removeAccountAttachment = (id, attId) => removeAttachmentFrom(setAccounts, id, attId);
  // Account Contact Log — same pattern as addVendorContact/removeVendorContact.
  // Returns the new contact's id so a caller can immediately link to it
  // (e.g. assigning a just-created contact to a fixed project role). The
  // record is built here rather than inside the updater so the id is known
  // synchronously and the updater stays pure.
  function addAccountContact(accountId, contact) {
    const record = { ...makeAccountContact(contact.role), ...contact };
    setAccounts(prev => prev.map(a => a.id === accountId ? { ...a, contacts: [...a.contacts, record] } : a));
    return record.id;
  }
  function removeAccountContact(accountId, contactId) {
    setAccounts(prev => prev.map(a => a.id === accountId ? { ...a, contacts: a.contacts.filter(c => c.id !== contactId) } : a));
  }
  const addVendorAttachment = (id, data) => addAttachmentTo(setVendors, id, data);
  const removeVendorAttachment = (id, attId) => removeAttachmentFrom(setVendors, id, attId);
  const addFreightForwarderAttachment = (id, data) => addAttachmentTo(setFreightForwarders, id, data);
  const removeFreightForwarderAttachment = (id, attId) => removeAttachmentFrom(setFreightForwarders, id, attId);
  const addSubcontractorAttachment = (id, data) => addAttachmentTo(setSubcontractors, id, data);
  const removeSubcontractorAttachment = (id, attId) => removeAttachmentFrom(setSubcontractors, id, attId);

  // ---- Projects ----
  // Turning privacy on adds the person doing it, so nobody can lock themselves
  // out in one click. Turning it off clears the list rather than keeping a
  // stale one that would silently apply if it were switched back on.
  function setProjectPrivacy(projectId, isPrivate, userIds) {
    updateProject(projectId, draft => {
      draft.isPrivate = !!isPrivate;
      if (isPrivate) {
        const list = (userIds || draft.visibleToUserIds || []).slice();
        if (effectiveUserId && list.indexOf(effectiveUserId) < 0) list.push(effectiveUserId);
        draft.visibleToUserIds = list;
        logAction(draft, `Job set to PRIVATE — visible to ${list.length} ${list.length === 1 ? 'person' : 'people'} (and Admins).`);
      } else {
        draft.visibleToUserIds = [];
        logAction(draft, 'Job privacy removed — visible to everyone again.');
      }
    });
  }
  function addProject(data) {
    const p = {
      id: uid('proj'),
      // A job is open unless someone deliberately restricts it.
      isPrivate: false, visibleToUserIds: [],
      // Sales tax follows the jurisdiction the job is installed in.
      taxRatePct: 0, taxExempt: false, taxExemptRef: '', taxExemptForm: null, taxExemptExpiry: null, taxNote: '',
      projectNumber: data.projectNumber,
      name: data.name,
      accountId: data.accountId,
      deleteRequest: null,
      department: data.department,
      companyDepartment: data.companyDepartment && data.companyDepartment.length ? data.companyDepartment : ['Interiors'],
      complexity: data.complexity,
      pipelineStatus: data.pipelineStatus,
      displayImageUrl: null,
      address: data.address || '',
      projectType: data.projectType || 'Residential',
      sizeSqFt: data.sizeSqFt ? Number(data.sizeSqFt) : null,
      unitQuantity: data.unitQuantity ? Number(data.unitQuantity) : null,
      buildingStories: data.buildingStories ? Number(data.buildingStories) : null,
      laborType: data.laborType || 'Standard',
      notes: '',
      contacts: {
        'General Contractor': { company: '', person: '', phone: '', email: '' },
        Owner: { company: '', person: '', phone: '', email: '' },
        Developer: { company: '', person: '', phone: '', email: '' },
        Architect: { company: '', person: '', phone: '', email: '' },
        Designer: { company: '', person: '', phone: '', email: '' },
        'Billing Contact': { company: '', person: '', phone: '', email: '' },
        'Additional Contact': { company: '', person: '', phone: '', email: '' },
      },
      team: {},
      additionalContacts: [],
      meetings: [],
      jobsiteVisits: [],
      // Project-level chronology (Lead Review/Take-Off/Quote Prep/Quote
      // Revision) — tracked once per project, not duplicated per scope.
      chronology: instantiateChronology(todayISO(), data.complexity),
      scopes: [],
      documents: [],
      drawingSets: [],
      takeOffs: [],
      renderSets: [],
      deliveries: [],
      exportDocuments: [],
      freightEstimates: [],
      freightPOs: [],
      installationRecords: [],
      dailyFieldReports: [],
      fieldIssues: [],
      materialReceipts: [],
      punchItems: [],
      fieldMeasurements: [],
      fieldMeasurementNA: {},
      financialIssues: [],
      applianceInstances: [],
      fixtureInstances: [],
      proformaInvoices: [],
      sov: [],
      applications: [],
      aiaHeaderDefaults: makeAiaHeaderDefaults(),
      productionRecords: [],
      quoteRevisions: [],
      quotationFollowUps: [],
      contractFile: null,
      contractFileUrl: null,
      contractSignedDate: null,
      changeOrders: [],
      paymentTerms: [
        { id: uid('pt'), label: 'Deposit', pct: 50, trigger: 'Contract Signing', status: 'Not Due' },
        { id: uid('pt'), label: 'Prior to Ship from Manufacture', pct: 40, trigger: 'Production Release', status: 'Not Due' },
        { id: uid('pt'), label: 'Upon Delivery to Jobsite', pct: 10, trigger: 'Delivery to Jobsite', status: 'Not Due' },
      ],
      retainagePct: 0,
      paymentRequisitions: [],
      vendorEstimates: [],
      purchaseOrders: [],
      tasks: [],
      issues: [],
      changeLog: [{ id: uid('log'), date: todayISO(), role: currentRole, user: currentUserName, action: `Project created. Document storage folder created. Team notified.` }],
      originalContractValue: 0,
      estimatedCost: 0,
      actualCostToDate: 0,
      health: 'Green',
    };
    setProjects(prev => [...prev, p]);
    return p;
  }

  // Demo Scenario (§ team walkthrough request) — one complete, realistic
  // project touching every major module, for demoing the app without
  // clicking through every workflow by hand first. Removes any previous run
  // (matched by its fixed project number) before adding a fresh one, so
  // re-running before another demo never leaves duplicates behind.
  // Shared by loadDemoScenario/removeDemoScenario — also purges any
  // warehouseMaterials/inventoryTransactions/materialAllocations created by
  // actually clicking "Receive at Warehouse" on the demo container during a
  // rehearsal (they'd otherwise survive a reset as orphaned inventory,
  // matched by name against nothing real, discovered when a leftover "23
  // units" of a demo material showed up after only one rehearsal receipt).
  // Matched by relatedPiId/containerId against the run being removed, never
  // by name — so a real material that happens to share a name is untouched.
  function purgeDemoScenario(existing, existingContainer) {
    const piIds = (existing.proformaInvoices || []).map(pi => pi.id);
    const orphanMaterialIds = warehouseMaterials.filter(m => piIds.includes(m.relatedPiId)).map(m => m.id);
    setAccounts(prev => prev.filter(a => a.id !== existing.accountId));
    setProjects(prev => prev.filter(p => p.id !== existing.id));
    setExportContainers(prev => prev.filter(c => !(c.shipments || []).some(s => s.projectId === existing.id)));
    setVendors(prev => prev.filter(v => v.name !== 'Coastal Cabinetry Supply Co.'));
    setSubcontractors(prev => prev.filter(s => s.companyName !== 'Apex Installation Group'));
    setTeamDirectory(prev => prev.filter(p => p.username !== 'druiz.demo' && p.username !== 'mbell.demo'));
    setMaterialAllocations(prev => prev.filter(a => a.projectId !== existing.id));
    setInventoryTransactions(prev => prev.filter(t => !(existingContainer && t.containerId === existingContainer.id)));
    if (orphanMaterialIds.length) setWarehouseMaterials(prev => prev.filter(m => !orphanMaterialIds.includes(m.id)));
  }
  function loadDemoScenario() {
    const existing = projects.find(p => p.projectNumber === 'LI-DEMO-001');
    if (existing) {
      const existingContainer = exportContainers.find(c => (c.shipments || []).some(s => s.projectId === existing.id));
      purgeDemoScenario(existing, existingContainer);
    }
    const scenario = buildDemoScenario({ teamDirectory, scopeLibrary, warehouseId: warehouses[0].id });
    setAccounts(prev => [...prev, scenario.account]);
    setVendors(prev => [...prev, scenario.vendor]);
    setSubcontractors(prev => [...prev, scenario.subcontractor]);
    setTeamDirectory(prev => [...prev, scenario.clientLogin, scenario.subcontractorLogin]);
    setProjects(prev => [...prev, scenario.project]);
    setExportContainers(prev => [...prev, scenario.container]);
    return scenario.project.id;
  }
  function removeDemoScenario() {
    const existing = projects.find(p => p.projectNumber === 'LI-DEMO-001');
    if (!existing) return;
    const existingContainer = exportContainers.find(c => (c.shipments || []).some(s => s.projectId === existing.id));
    purgeDemoScenario(existing, existingContainer);
  }
  function setProjectImage(projectId, dataUrl) {
    updateProject(projectId, draft => { draft.displayImageUrl = dataUrl; logAction(draft, 'Updated project display picture.'); });
  }
  function removeProjectImage(projectId) {
    updateProject(projectId, draft => { draft.displayImageUrl = null; logAction(draft, 'Removed project display picture.'); });
  }

  function reportDelay(projectId, scopeId, stageId, days, reason, note) {
    setProjects(prev => prev.map(p => p.id === projectId ? applyDelayCascade(p, scopeId, stageId, days, reason, note, currentRole, currentUserName) : p));
  }
  function reportWindowScheduleDelay(projectId, scopeId, nodeId, days, reason, note) {
    setProjects(prev => prev.map(p => p.id === projectId ? applyWindowScheduleDelay(p, scopeId, nodeId, days, reason, note, currentRole, currentUserName) : p));
  }

  function startStage(projectId, scopeId, stageId) {
    const project = projects.find(p => p.id === projectId);
    const scope = project && findScope(project, scopeId);
    const stage = scope && scope.stages.find(s => s.id === stageId);
    if (stage) {
      const gate = canStartStage(project, stage);
      if (!gate.ok) { window.alert(gate.reason); return; }
    }
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      pushStageHistory(scope, stageId);
      const st = scope.stages.find(s => s.id === stageId);
      st.actualStart = todayISO();
      st.status = 'In Progress';
      logAction(draft, `Started stage "${st.name}" on ${scope.name}.`);
    });
  }

  function completeStage(projectId, scopeId, stageId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      pushStageHistory(scope, stageId);
      const idx = scope.stages.findIndex(s => s.id === stageId);
      const st = scope.stages[idx];
      const completedOn = todayISO();
      st.actualCompletion = completedOn;
      if (!st.actualStart) st.actualStart = completedOn;
      st.status = 'Completed';
      if (idx + 1 < scope.stages.length) {
        const nxt = scope.stages[idx + 1];
        nxt.plannedStart = completedOn;
        nxt.plannedDue = addDays(completedOn, nxt.duration);
        nxt.status = 'In Progress';
        nxt.actualStart = completedOn;
      }
      logAction(draft, `Completed stage "${st.name}" on ${scope.name}.` + (idx + 1 < scope.stages.length ? ` Next stage "${scope.stages[idx + 1].name}" activated, due date recalculated.` : ' Scope complete.'));
      const derived = deriveMinimumPipelineStatus(draft);
      if (PIPELINE_STATUSES.indexOf(derived) > PIPELINE_STATUSES.indexOf(draft.pipelineStatus)) draft.pipelineStatus = derived;
    });
  }

  // Stages are assigned to a specific person, not just a role label (§
  // scope/schedule request) — the old responsibleRole never resolved to an
  // actual person anywhere, so nothing could surface "your" scope work in a
  // personal view. This is purely additive to the stage object.
  function assignStageUser(projectId, scopeId, stageId, userId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const st = scope.stages.find(s => s.id === stageId);
      st.assignedUserId = userId || null;
    });
  }

  // Undoes the most recent Start / Complete / Report Delay on this scope's
  // stages (§ scopes/schedule request), by popping the last full snapshot
  // pushStageHistory took before that action and restoring it wholesale —
  // simpler and safer than hand-writing a reverse transition for each of the
  // three actions, especially since Report Delay's cascade touches every
  // downstream stage in one go.
  function undoStageChange(projectId, scopeId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope || !scope.stageHistory || !scope.stageHistory.length) return;
      const entry = scope.stageHistory.pop();
      const st = scope.stages.find(s => s.id === entry.stageId);
      scope.stages = entry.stages;
      scope.projectedCompletion = entry.projectedCompletion;
      logAction(draft, `Undid last status change on stage "${st ? st.name : ''}" — ${scope.name}.`);
    });
  }
  function addRevisionStage(projectId, scopeId, familyAnchorKey, labelOverride) {
    setProjects(prev => prev.map(p => p.id === projectId ? insertRevisionStage(p, scopeId, familyAnchorKey, currentRole, currentUserName, labelOverride) : p));
  }

  // ---- Chronology (Lead Review/Take-Off/Quote Prep/Quote Revision) — one
  // shared instance per project. Exact mechanical counterparts to
  // startStage/completeStage/reportDelay/undoStageChange above, operating on
  // project.chronology instead of a scope's own stages array.
  function reportChronologyDelay(projectId, stageId, days, reason, note) {
    setProjects(prev => prev.map(p => p.id === projectId ? applyChronologyDelayCascade(p, stageId, days, reason, note, currentRole, currentUserName) : p));
  }
  function startChronologyStage(projectId, stageId) {
    const project = projects.find(p => p.id === projectId);
    const stage = project && project.chronology.find(s => s.id === stageId);
    if (stage) {
      const gate = canStartStage(project, stage);
      if (!gate.ok) { window.alert(gate.reason); return; }
    }
    updateProject(projectId, draft => {
      pushChronologyHistory(draft, stageId);
      const st = draft.chronology.find(s => s.id === stageId);
      st.actualStart = todayISO();
      st.status = 'In Progress';
      logAction(draft, `Started stage "${st.name}" on Lead, Take-Off & Quotes.`);
    });
  }
  function completeChronologyStage(projectId, stageId) {
    updateProject(projectId, draft => {
      pushChronologyHistory(draft, stageId);
      const idx = draft.chronology.findIndex(s => s.id === stageId);
      const st = draft.chronology[idx];
      const completedOn = todayISO();
      st.actualCompletion = completedOn;
      if (!st.actualStart) st.actualStart = completedOn;
      st.status = 'Completed';
      if (idx + 1 < draft.chronology.length) {
        const nxt = draft.chronology[idx + 1];
        nxt.plannedStart = completedOn;
        nxt.plannedDue = addDays(completedOn, nxt.duration);
        nxt.status = 'In Progress';
        nxt.actualStart = completedOn;
      }
      logAction(draft, `Completed stage "${st.name}" on Lead, Take-Off & Quotes.` + (idx + 1 < draft.chronology.length ? ` Next stage "${draft.chronology[idx + 1].name}" activated.` : ' Lead, Take-Off & Quotes complete.'));
      const derived = deriveMinimumPipelineStatus(draft);
      if (PIPELINE_STATUSES.indexOf(derived) > PIPELINE_STATUSES.indexOf(draft.pipelineStatus)) draft.pipelineStatus = derived;
    });
  }
  function assignChronologyUser(projectId, stageId, userId) {
    updateProject(projectId, draft => {
      const st = draft.chronology.find(s => s.id === stageId);
      st.assignedUserId = userId || null;
    });
  }
  function undoChronologyChange(projectId) {
    updateProject(projectId, draft => {
      if (!draft.chronologyHistory || !draft.chronologyHistory.length) return;
      const entry = draft.chronologyHistory.pop();
      const st = draft.chronology.find(s => s.id === entry.stageId);
      draft.chronology = entry.stages;
      draft.chronologyProjectedCompletion = entry.projectedCompletion;
      logAction(draft, `Undid last status change on stage "${st ? st.name : ''}" — Lead, Take-Off & Quotes.`);
    });
  }

  // ---- Personal To-Do / Calendar items (§ My To-Do request) — cross-project,
  // per-user; not nested inside a project since a personal Event or To-Do
  // isn't necessarily tied to any one job. ----
  // Repeat is materialized as discrete records up front (not a virtual
  // recurrence rule expanded at read time) — simpler and matches how every
  // other list in this app already works; each occurrence shares a
  // recurrenceGroupId so they can be told apart from unrelated items later.
  // data.userId lets a caller create an item owned by someone OTHER than
  // whoever's logged in (e.g. assigning a follow-up reminder to a
  // teammate) — createdBy still records who actually did the assigning.
  function addPersonalItem(data) {
    const occurrences = data.repeat && data.repeat !== 'None' ? Math.max(1, Number(data.occurrences) || 1) : 1;
    const groupId = occurrences > 1 ? uid('recur') : null;
    const stepDays = { Daily: 1, Weekly: 7, Monthly: 30 }[data.repeat] || 0;
    const ownerId = data.userId || currentUser.id;
    const items = Array.from({ length: occurrences }, (_, i) => makePersonalItem(
      { ...data, date: data.date ? addDays(data.date, stepDays * i) : data.date, recurrenceGroupId: groupId },
      ownerId, currentUserName
    ));
    setPersonalItems(prev => [...prev, ...items]);
  }
  function setPersonalItemStatus(itemId, status) {
    setPersonalItems(prev => prev.map(i => i.id === itemId ? { ...i, status } : i));
  }
  function updatePersonalItem(itemId, fields) {
    setPersonalItems(prev => prev.map(i => i.id === itemId ? { ...i, ...fields } : i));
  }
  function removePersonalItem(itemId) {
    setPersonalItems(prev => prev.filter(i => i.id !== itemId));
  }
  function addScope(projectId, { name, familyName, startDate, windowSystemId, windowFinish, windowGlass, scopeType }) {
    updateProject(projectId, draft => {
      // Window-classified scopes get the reduced WINDOW_STAGE_DEFS set (their
      // drawing/production/shipping milestones live in the Window Schedule
      // graph instead); interiors scopes get their family's own durations
      // from the Interiors Lead-Time Library.
      // Supply Only drops the installation and punch stages — LEON is not on
      // site for this scope, so those milestones do not exist.
      const type = effectiveScopeType(familyName, scopeLibrary, scopeType);
      const stages = instantiateStages(startDate, draft.complexity, stageDefsForScope(familyName, scopeLibrary, interiorLeadTimeLibrary, type));
      applyTeamDefaultsToStages(stages, projectTeamFor(draft, familyDepartment(familyName, scopeLibrary)));
      const fam = scopeLibrary.find(f => f.name === familyName);
      const selections = {};
      if (fam) fam.categories.forEach(c => { selections[c.id] = null; });
      // Field set mirrors makeScope (data.jsx) — kept as its own literal
      // (rather than calling makeScope) because this needs the live,
      // admin-editable `scopeLibrary` state, not the static DEFAULT_SCOPE_LIBRARY
      // makeScope reads from.
      const scope = { id: uid('scope'), name, familyName, scopeType: type, department: familyDepartment(familyName, scopeLibrary), selections, selectionsLocked: false, materialLinks: {}, mainAreaName: '', selectionAreas: [], supplierFinishes: {}, selectionRevisions: [], stages, stageHistory: [], documents: [], submittals: [], clientResponses: [], profitability: makeScopeProfitability(), quantity: null, unit: 'Units' };
      // Window/Exterior Door System scopes additionally get a windowSchedule
      // (§ Window Schedule template) — purely additive alongside `stages`
      // above, which stays on the normal generic path so every existing
      // reader of scope.stages keeps working unchanged for these scopes too.
      if (isWindowSystemFamily(familyName, scopeLibrary)) {
        scope.windowSchedule = buildWindowSchedule({ startDate, systemId: windowSystemId, finish: windowFinish, glass: windowGlass, windowLeadTimeLibrary });
        computeWindowSchedule(scope.windowSchedule);
      }
      draft.scopes.push(scope);
      logAction(draft, `Added scope "${name}" (${familyName}) — ${stages.length} stage records generated, due dates calculated.`);
    });
  }

  // The supplier finish (An Cuong) chosen for one selection category. Stores
  // only a small reference, not the catalog record — see makeSupplierFinishRef.
  function setSupplierFinish(projectId, scopeId, categoryId, rec) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (scope.selectionsLocked) return;
      if (!scope.supplierFinishes) scope.supplierFinishes = {};
      const ref = makeSupplierFinishRef(rec);
      if (ref) scope.supplierFinishes[categoryId] = ref;
      else delete scope.supplierFinishes[categoryId];
      const { category } = findCategoryPath(scopeLibrary, categoryId);
      logAction(draft, ref
        ? `Set supplier finish "${ref.name} (${ref.code})" for ${category ? category.name : 'selection'} on ${scope.name}.`
        : `Cleared supplier finish for ${category ? category.name : 'selection'} on ${scope.name}.`);
    });
  }
  // The application area a GROUP of selections covers. The main group's name
  // lives on the scope; each additional group carries its own. Excluded from
  // the selections lock — naming where a set of finishes applies is a
  // clarification, not a re-selection.
  function setScopeMainAreaName(projectId, scopeId, name) {
    updateProject(projectId, draft => {
      findScope(draft, scopeId).mainAreaName = name;
    });
  }
  function renameSelectionArea(projectId, scopeId, areaId, name) {
    updateProject(projectId, draft => {
      const area = findScope(draft, scopeId).selectionAreas.find(a => a.id === areaId);
      if (area) area.name = name;
    });
  }
  // Supplier finish on one of the ADDITIONAL groups (the main group's
  // equivalent is setSupplierFinish above).
  function setAreaSupplierFinish(projectId, scopeId, areaId, categoryId, rec) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (scope.selectionsLocked) return;
      const area = scope.selectionAreas.find(a => a.id === areaId);
      if (!area) return;
      if (!area.supplierFinishes) area.supplierFinishes = {};
      const ref = makeSupplierFinishRef(rec);
      if (ref) area.supplierFinishes[categoryId] = ref;
      else delete area.supplierFinishes[categoryId];
      logAction(draft, ref
        ? `Set supplier finish "${ref.name} (${ref.code})" in area "${area.name}" on ${scope.name}.`
        : `Cleared a supplier finish in area "${area.name}" on ${scope.name}.`);
    });
  }
  function setSelection(projectId, scopeId, categoryId, optionId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (scope.selectionsLocked) return;
      scope.selections[categoryId] = optionId || null;
      const { category } = findCategoryPath(scopeLibrary, categoryId);
      const opt = optionId ? (category?.options.find(o => o.id === optionId)) : null;
      logAction(draft, `Set selection "${category ? category.name : ''}" = ${opt ? opt.name : '(cleared)'} on ${scope.name}.`);
    });
  }
  // Locking a scope's selections (§ lock selections request) freezes every
  // category's dropdown against casual edits once the client/team has
  // signed off — setSelection itself refuses to write while locked, as a
  // real boundary, not just a disabled control in the UI. The revision trail
  // is keyed to the LOCK event, not to every individual field edit: locking
  // snapshots the current selections as the next numbered revision (the
  // first lock = Revision 1), so free editing while unlocked doesn't spam
  // the history — only "this is the version we committed to" moments do.
  // Unlocking ("Request Revision") reopens the fields but records no
  // revision itself; the next lock after that becomes Revision 2, and so on.
  function setScopeSelectionsLocked(projectId, scopeId, locked, meeting) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      scope.selectionsLocked = locked;
      if (locked) {
        const nextRevision = (scope.selectionRevisions && scope.selectionRevisions.length ? Math.max(...scope.selectionRevisions.map(r => r.revisionNumber)) : 0) + 1;
        const where = meeting && meeting.held
          ? ` at a selection meeting on ${fmtDate(meeting.date)}${meeting.location ? ` — ${meeting.location}` : ''}${meeting.clientPresent ? ', client present' : ', client not present'}`
          : '';
        pushSelectionRevision(scope, `Selections locked — Revision ${nextRevision} captured on ${scope.name}${where}.`, currentUserName, meeting);
      }
      logAction(draft, `${locked ? 'Locked' : 'Unlocked (revision requested for)'} selections on ${scope.name}.`);
    });
  }
  // Named sub-areas within a scope (e.g. "Primary Bath Shower Surround"
  // under a Tile scope), each carrying its own selections when a single
  // set of choices for the whole scope isn't enough.
  function addSelectionArea(projectId, scopeId, name) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const fam = scopeLibrary.find(f => f.name === scope.familyName);
      const selections = {};
      if (fam) fam.categories.forEach(c => { selections[c.id] = null; });
      scope.selectionAreas.push({ id: uid('area'), name, selections, supplierFinishes: {} });
      logAction(draft, `Added selection area "${name}" to ${scope.name}.`);
    });
  }
  function removeSelectionArea(projectId, scopeId, areaId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const area = scope.selectionAreas.find(a => a.id === areaId);
      scope.selectionAreas = scope.selectionAreas.filter(a => a.id !== areaId);
      if (area) logAction(draft, `Removed selection area "${area.name}" from ${scope.name}.`);
    });
  }
  function setAreaSelection(projectId, scopeId, areaId, categoryId, optionId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const area = scope.selectionAreas.find(a => a.id === areaId);
      area.selections[categoryId] = optionId || null;
      const { category } = findCategoryPath(scopeLibrary, categoryId);
      const opt = optionId ? (category?.options.find(o => o.id === optionId)) : null;
      const change = `Set selection "${category ? category.name : ''}" = ${opt ? opt.name : '(cleared)'} on ${scope.name} — ${area.name}.`;
      logAction(draft, change);
      pushSelectionRevision(scope, change, currentUserName);
    });
  }

  function addDocument(projectId, scopeId, doc) {
    updateProject(projectId, draft => {
      const record = { id: uid('doc'), tier: 'scope', ...doc };
      const scope = findScope(draft, scopeId);
      scope.documents.push(record);
      logAction(draft, `Attached document "${doc.name}" (${doc.type}) to ${scope.name}.`);
    });
  }

  // ---- custom document sections under a scope ------------------------------
  // ---- AI take-off requests (Admin only) -----------------------------------
  function requestAiTakeoff(projectId, data) {
    updateProject(projectId, draft => {
      draft.aiTakeoffRequests = draft.aiTakeoffRequests || [];
      const req = makeAiTakeoffRequest(data, currentUserName);
      draft.aiTakeoffRequests.push(req);
      logAction(draft, `AI take-off requested for drawing set "${req.drawingSetName || '—'}".`);
    });
  }
  function updateAiTakeoff(projectId, reqId, fields) {
    updateProject(projectId, draft => {
      const r = (draft.aiTakeoffRequests || []).find(x => x.id === reqId);
      if (!r) return;
      const wasStatus = r.status;
      Object.assign(r, fields);
      if (fields.resultFileUrl) { r.deliveredBy = currentUserName; r.deliveredDate = todayISO(); r.status = 'Delivered'; }
      if (r.status !== wasStatus) logAction(draft, `AI take-off for "${r.drawingSetName || '—'}" marked ${r.status}.`);
    });
  }
  function removeAiTakeoff(projectId, reqId) {
    updateProject(projectId, draft => {
      const r = (draft.aiTakeoffRequests || []).find(x => x.id === reqId);
      if (!r) return;
      draft.aiTakeoffRequests = draft.aiTakeoffRequests.filter(x => x.id !== reqId);
      logAction(draft, `AI take-off request for "${r.drawingSetName || '—'}" removed.`);
    });
  }
  function addScopeDocumentSection(projectId, scopeId, name) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!Array.isArray(scope.documentSections)) scope.documentSections = [];
      scope.documentSections.push({ id: uid('docsec'), name, createdBy: currentUserName, createdDate: todayISO() });
      logAction(draft, `Added document section "${name}" to ${scope.name}.`);
    });
  }
  function renameScopeDocumentSection(projectId, scopeId, sectionId, name) {
    updateProject(projectId, draft => {
      const sec = (findScope(draft, scopeId).documentSections || []).find(x => x.id === sectionId);
      if (!sec) return;
      logAction(draft, `Document section "${sec.name}" renamed to "${name}".`);
      sec.name = name;
    });
  }
  // Deleting a section does NOT delete its documents — they fall back to the
  // scope's general list, where they stay findable. Losing files because a
  // heading was removed would be indefensible.
  function removeScopeDocumentSection(projectId, scopeId, sectionId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const sec = (scope.documentSections || []).find(x => x.id === sectionId);
      if (!sec) return;
      const moved = (scope.documents || []).filter(d => d.sectionId === sectionId);
      moved.forEach(d => { d.sectionId = null; });
      scope.documentSections = scope.documentSections.filter(x => x.id !== sectionId);
      logAction(draft, `Removed document section "${sec.name}" from ${scope.name}${moved.length ? ` — ${moved.length} document${moved.length === 1 ? '' : 's'} moved back to the scope's documents` : ''}.`);
    });
  }
  function removeScopeDocument(projectId, scopeId, docId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const d = (scope.documents || []).find(x => x.id === docId);
      if (!d) return;
      scope.documents = scope.documents.filter(x => x.id !== docId);
      logAction(draft, `Removed document "${d.name}" from ${scope.name}.`);
    });
  }
  function updateScopeDocument(projectId, scopeId, docId, fields) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const d = scope.documents.find(x => x.id === docId);
      Object.assign(d, fields);
      if (fields.fileUrl) logAction(draft, `Attached file to document "${d.name}".`);
    });
  }

  function addProjectDocument(projectId, doc) {
    updateProject(projectId, draft => {
      draft.documents.push({ id: uid('doc'), tier: 'project', ...doc });
      logAction(draft, `Attached document "${doc.name}" (${doc.type}).`);
    });
  }

  function addDrawingSet(projectId, data) {
    updateProject(projectId, draft => {
      draft.drawingSets.push({ id: uid('dwg'), status: 'Active', voidReason: null, voidedDate: null, voidedBy: null, ...data });
      logAction(draft, `Added Drawing Set "${data.name}" (Rev ${data.revision}) from ${data.source || 'unknown source'} — assigned to ${personName(teamDirectory, data.reviewerAssigneeId)} to review.`);
    });
  }
  // Drawing Sets can never be deleted, only voided with a reason (§ drawing
  // sets request) — the record and its history stay on file either way.
  function voidDrawingSet(projectId, dwgId, reason) {
    updateProject(projectId, draft => {
      const d = draft.drawingSets.find(x => x.id === dwgId);
      d.status = 'Void';
      d.voidReason = reason;
      d.voidedDate = todayISO();
      d.voidedBy = currentUserName;
      logAction(draft, `Voided Drawing Set "${d.name}" (Rev ${d.revision}): ${reason}`);
    });
  }
  function updateDrawingSet(projectId, dwgId, fields) {
    updateProject(projectId, draft => {
      const d = draft.drawingSets.find(x => x.id === dwgId);
      Object.assign(d, fields);
      if (fields.fileUrl) logAction(draft, `Attached file to Drawing Set "${d.name}".`);
    });
  }

  // A stage the app thinks just got finished, waiting on a yes/no. One at a
  // time and latest-wins: a queue of these would be a nag, and the person is
  // right there having just done the thing.
  const [stageSuggestion, setStageSuggestion] = useState(null);
  function suggestStage(projectId, { scopeId, stageKey, because }) {
    const project = projects.find(p => p.id === projectId);
    const stage = findStageToSuggest(project, scopeId || null, stageKey);
    if (!stage) return;                                  // no such stage, or already complete
    setStageSuggestion({ projectId, scopeId: scopeId || null, stageId: stage.id, stageName: stage.name, because });
  }
  function acceptStageSuggestion() {
    const sg = stageSuggestion;
    if (!sg) return;
    if (sg.scopeId) completeStage(sg.projectId, sg.scopeId, sg.stageId);
    else completeChronologyStage(sg.projectId, sg.stageId);
    setStageSuggestion(null);
  }

  function addTakeOff(projectId, data) {
    updateProject(projectId, draft => {
      draft.takeOffs.push({ id: uid('to'), ...data });
      logAction(draft, `Added Take-Off "${data.name}" (Rev ${data.revision}).`);
    });
    suggestStage(projectId, { stageKey: 'take_off', because: `Take-Off "${data.name}" (Rev ${data.revision}) was added` });
  }
  function removeTakeOff(projectId, toId) {
    updateProject(projectId, draft => {
      const t = draft.takeOffs.find(x => x.id === toId);
      draft.takeOffs = draft.takeOffs.filter(x => x.id !== toId);
      if (t) logAction(draft, `Removed Take-Off "${t.name}".`);
    });
  }
  function updateTakeOff(projectId, toId, fields) {
    updateProject(projectId, draft => {
      const t = draft.takeOffs.find(x => x.id === toId);
      Object.assign(t, fields);
      if (fields.fileUrl) logAction(draft, `Attached file to Take-Off "${t.name}".`);
    });
  }

  // What a take-off was measured FROM — the finish schedule, the client's door
  // schedule, the kitchen and bathroom layouts. Filed against the take-off
  // itself rather than the project's general documents, because the question
  // asked months later is "what was Rev 2 counted off", and a document library
  // cannot answer that.
  function addTakeOffAttachment(projectId, toId, data) {
    updateProject(projectId, draft => {
      const t = draft.takeOffs.find(x => x.id === toId);
      if (!t) return;
      if (!t.attachments) t.attachments = [];
      const att = makeTakeoffAttachment(data, currentUserName);
      t.attachments.push(att);
      logAction(draft, `Filed ${att.picture ? 'a picture' : 'a document'} under `
        + `${takeoffDocKind(att.kind).label} on Take-Off "${t.name}" (Rev ${t.revision}).`);
    });
  }
  function updateTakeOffAttachment(projectId, toId, attId, fields) {
    updateProject(projectId, draft => {
      const t = draft.takeOffs.find(x => x.id === toId);
      const a = t && (t.attachments || []).find(x => x.id === attId);
      if (a) Object.assign(a, fields);
      // A caption or a re-filing is not worth a change-log line every keystroke;
      // what matters on the record is that the file arrived and that it left.
    });
  }
  function removeTakeOffAttachment(projectId, toId, attId) {
    updateProject(projectId, draft => {
      const t = draft.takeOffs.find(x => x.id === toId);
      if (!t) return;
      const a = (t.attachments || []).find(x => x.id === attId);
      t.attachments = (t.attachments || []).filter(x => x.id !== attId);
      if (a) logAction(draft, `Removed ${a.name || 'an attachment'} from `
        + `Take-Off "${t.name}" (Rev ${t.revision}).`);
    });
  }

  // ── Project closeout ────────────────────────────────────────────────────
  function withCloseout(projectId, fn, note) {
    updateProject(projectId, draft => {
      if (!draft.closeout) draft.closeout = makeCloseout();
      fn(draft.closeout, draft);
      if (note) logAction(draft, note);
    });
  }
  function updateCloseout(projectId, fields) {
    withCloseout(projectId, co => Object.assign(co, fields),
      fields.status ? `Closeout status set to ${fields.status}.` : null);
  }
  function setCloseoutItem(projectId, itemId, fields) {
    withCloseout(projectId, (co, draft) => {
      const it = co.checklist.find(x => x.id === itemId);
      if (!it) return;
      const wasState = it.state;
      Object.assign(it, fields);
      if (fields.state && fields.state !== wasState) {
        it.by = fields.state === 'Open' ? '' : currentUserName;
        it.date = fields.state === 'Open' ? null : todayISO();
        logAction(draft, `Closeout: "${it.label}" marked ${fields.state}.`);
      }
    });
  }
  function addCloseoutItem(projectId, label, group) {
    withCloseout(projectId, co => {
      co.checklist.push(makeCloseoutItem({ label, group: group || 'Record' }));
    }, `Closeout: added checklist item "${label}".`);
  }
  function removeCloseoutItem(projectId, itemId) {
    withCloseout(projectId, co => {
      const it = co.checklist.find(x => x.id === itemId);
      // Only an item this job added can be removed; a seeded one is marked N/A
      // instead, so the standard list stays comparable across jobs.
      if (it && it.custom) co.checklist = co.checklist.filter(x => x.id !== itemId);
    });
  }
  function addCloseoutPhotos(projectId, files) {
    withCloseout(projectId, co => {
      files.forEach(f => co.photos.push(makeCloseoutPhoto(f.file, f.fileUrl, { addedBy: currentUserName })));
    }, `Closeout: added ${files.length} finished photo${files.length === 1 ? '' : 's'}.`);
  }
  function updateCloseoutPhoto(projectId, photoId, fields) {
    withCloseout(projectId, co => {
      const ph = co.photos.find(x => x.id === photoId);
      if (ph) Object.assign(ph, fields);
    });
  }
  function removeCloseoutPhoto(projectId, photoId) {
    withCloseout(projectId, co => {
      co.photos = co.photos.filter(x => x.id !== photoId);
      if (co.portfolio.coverPhotoId === photoId) co.portfolio.coverPhotoId = null;
    });
  }
  function addCloseoutDoc(projectId, data) {
    withCloseout(projectId, co => co.documents.push(makeCloseoutDoc(data, currentUserName)),
      `Closeout: filed "${data.name || data.type}".`);
  }
  function updateCloseoutDoc(projectId, docId, fields) {
    withCloseout(projectId, co => {
      const d = co.documents.find(x => x.id === docId);
      if (d) Object.assign(d, fields);
    });
  }
  function removeCloseoutDoc(projectId, docId) {
    withCloseout(projectId, co => { co.documents = co.documents.filter(x => x.id !== docId); });
  }
  function addLesson(projectId, data) {
    withCloseout(projectId, co => co.lessons.push(makeLesson(data, currentUserName)),
      `Closeout: logged a lesson learned (${data.category}).`);
  }
  function updateLesson(projectId, lessonId, fields) {
    withCloseout(projectId, co => {
      const l = co.lessons.find(x => x.id === lessonId);
      if (l) Object.assign(l, fields);
    });
  }
  function removeLesson(projectId, lessonId) {
    withCloseout(projectId, co => { co.lessons = co.lessons.filter(x => x.id !== lessonId); });
  }
  // Publishing to the Finished Projects library is separate from closing the
  // job: a job can be finished and still not be something you would show.
  function setCloseoutPortfolio(projectId, fields) {
    withCloseout(projectId, (co, draft) => {
      const was = co.portfolio.published;
      Object.assign(co.portfolio, fields);
      if (fields.published !== undefined && fields.published !== was) {
        co.portfolio.publishedBy = fields.published ? currentUserName : '';
        co.portfolio.publishedDate = fields.published ? todayISO() : null;
        logAction(draft, fields.published
          ? 'Published to the Finished Projects library.'
          : 'Removed from the Finished Projects library.');
      }
    });
  }
  function closeProject(projectId) {
    withCloseout(projectId, (co, draft) => {
      co.status = 'Closed';
      co.closedDate = todayISO();
      co.closedBy = currentUserName;
      if (!co.finalCompletionDate) co.finalCompletionDate = todayISO();
      logAction(draft, `Project closed out by ${currentUserName}.`);
    });
  }
  function reopenCloseout(projectId) {
    withCloseout(projectId, (co, draft) => {
      co.status = 'In Progress'; co.closedDate = null; co.closedBy = '';
      logAction(draft, 'Closeout reopened.');
    });
  }

  // ── Quote Analysis (Interiors) ──────────────────────────────────────────
  // A pricing draft, and ONLY that. It deliberately does not create scopes,
  // stages or a schedule: this is a bid, not a job, and a quote that quietly
  // built a production schedule would be very hard to unpick when the client
  // says no. Turning an accepted analysis into contracted scopes is a separate,
  // later step, done on purpose.
  function withQuoteAnalysis(projectId, qaId, fn, note) {
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      if (!qa) return;
      fn(qa, draft);
      if (note) logAction(draft, `Quote Analysis "${qa.name}": ${note}`);
    });
  }
  function loadDemoQuoteAnalysis(projectId) {
    const qa = buildDemoQuoteAnalysis(currentUserName);
    updateProject(projectId, draft => {
      if (!Array.isArray(draft.quoteAnalyses)) draft.quoteAnalyses = [];
      draft.quoteAnalyses.unshift(qa);
      logAction(draft, `Loaded the example Quote Analysis. It prices nothing real — delete it when done.`);
    });
    return qa.id;
  }
  function createQuoteAnalysis(projectId, data) {
    const qa = makeQuoteAnalysis(data, currentUserName);
    updateProject(projectId, draft => {
      if (!Array.isArray(draft.quoteAnalyses)) draft.quoteAnalyses = [];
      draft.quoteAnalyses.unshift(qa);
      logAction(draft, `Generated Quote Analysis "${qa.name}" from ${qa.source === 'excel' ? 'an imported workbook' : qa.source === 'takeoff' ? `take-off "${qa.sourceRef}"` : 'a blank draft'}.`);
    });
    return qa.id;
  }
  function updateQuoteAnalysis(projectId, qaId, fields) {
    withQuoteAnalysis(projectId, qaId, qa => Object.assign(qa, fields),
      Object.keys(fields).includes('status') ? `status set to ${fields.status}.` : null);
  }
  function removeQuoteAnalysis(projectId, qaId) {
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      draft.quoteAnalyses = (draft.quoteAnalyses || []).filter(q => q.id !== qaId);
      if (qa) logAction(draft, `Deleted Quote Analysis "${qa.name}".`);
    });
  }
  // A new revision is a copy, so the version that was sent stays exactly as it
  // was sent — the old one is marked Superseded rather than edited.
  // ── Quotation → contract ──────────────────────────────────────────────────
  // The one step that turns a priced quotation into real work. Until now the
  // Quote Analysis deliberately wrote nothing but itself — a bid is not a job,
  // and a quote that quietly built a production schedule would be very hard to
  // unpick when the client says no. This is that step, made explicit.
  //
  // What it does: one project SCOPE per quoted scope, each with its own stage
  // schedule from the family's lead times, and the quoted sell recorded as that
  // scope's contract value. What it does NOT do: touch the quotation. The
  // quotation stays exactly as it was issued — it is the record of what was
  // agreed, and a scope that later changes must not rewrite it.
  function convertQuoteToScopes(projectId, qaId, opts) {
    const o = opts || {};
    const startDate = o.startDate || todayISO();
    let made = [];
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      if (!qa) return;
      // Converting twice would double the job. The analysis records that it was
      // converted, and the guard reads that rather than trusting the caller.
      if (qa.convertedDate) return;
      const chosen = (qa.sections || []).filter(sec => !o.only || o.only.indexOf(sec.id) >= 0);
      chosen.forEach(sec => {
        const familyName = sec.scopeKey || sec.name;
        // The quote's own sold-as answer decides the scope type, so the
        // schedule matches what was actually sold: a Supply Only scope has no
        // installation stages because LEON is not on site for it.
        const wanted = sec.kind === 'labor' ? 'Labor Only'
          : sec.kind === 'combined' ? COMBINED_SCOPE_TYPE : 'Supply Only';
        const type = effectiveScopeType(familyName, scopeLibrary, wanted);
        const stages = instantiateStages(startDate, draft.complexity,
          stageDefsForScope(familyName, scopeLibrary, interiorLeadTimeLibrary, type));
        applyTeamDefaultsToStages(stages, projectTeamFor(draft, familyDepartment(familyName, scopeLibrary)));
        const fam = scopeLibrary.find(f => f.name === familyName);
        const selections = {};
        if (fam) fam.categories.forEach(c => { selections[c.id] = null; });
        const t = quoteSectionTotals(sec, qa);
        const prof = makeScopeProfitability();
        // The quoted sell becomes this scope's contract value, and the quoted
        // cost its budget — so the job starts out measured against what was
        // actually sold rather than against nothing.
        prof.contractValue = t.sell;
        prof.budget = Object.assign({}, prof.budget, {
          vendorCost: t.mat, laborCost: t.labor + t.install,
          freight: t.freight, duty: t.duty, overhead: t.overhead,
        });
        const scope = {
          id: uid('scope'), name: sec.name, familyName, scopeType: type,
          department: familyDepartment(familyName, scopeLibrary),
          selections, selectionsLocked: false, materialLinks: {}, mainAreaName: '',
          selectionAreas: [], supplierFinishes: {}, selectionRevisions: [],
          stages, stageHistory: [], documents: [], submittals: [], clientResponses: [],
          profitability: prof,
          quantity: (sec.lines || []).reduce((a, l) => a + (l.excluded ? 0 : (Number(l.qty) || 0)), 0) || null,
          unit: sec.uom || 'Units',
          // Where this scope came from, so the job can be traced back to the
          // quotation that sold it.
          fromQuoteAnalysisId: qa.id, fromQuoteSectionId: sec.id, fromQuoteRevision: qa.revision,
        };
        if (isWindowSystemFamily(familyName, scopeLibrary)) {
          scope.windowSchedule = buildWindowSchedule({ startDate, windowLeadTimeLibrary });
          computeWindowSchedule(scope.windowSchedule);
        }
        draft.scopes.push(scope);
        made.push(scope.name);
      });
      qa.convertedDate = todayISO();
      qa.convertedBy = currentUserName;
      logAction(draft, `Quotation "${qa.name}" Rev ${qa.revision} converted to contract — ${made.length} scope${made.length === 1 ? '' : 's'} created with their stage schedules: ${made.join(', ')}.`);
    });
    return made;
  }

  // "I am done — save this as the revision." Freezes it: an ISSUED revision is
  // the record of what was quoted and stops being editable, which is the whole
  // reason a revision is worth having. Editing afterwards forks Rev N+1 rather
  // than rewriting what was already sent.
  // Issuing a quotation does TWO things, and the second is the one that was
  // missing. It freezes the analysis, and it files a real QUOTE REVISION on the
  // job — because an analysis is our internal working, and what the client is
  // holding is a quotation. The Quotes tab is where a quotation is chased,
  // superseded and eventually converted to a contract, and until now nothing
  // put one there: the analysis and the quote record were two unconnected
  // halves of the same event.
  // Turn one analysis revision into a QUOTATION. This is the step that was
  // missing and the reason the two halves felt like one screen: an analysis is
  // our internal working — what a scope costs, what it earns — and a quotation
  // is the document the client reads. Pressing this moves the work across:
  // from here on the deck is arranged, issued and chased under Quotes, and the
  // analysis goes back to being the pricing behind it.
  function createQuoteFromAnalysis(projectId, qaId, opts) {
    const o = opts || {};
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      if (!qa) return;
      const list = draft.quoteRevisions || (draft.quoteRevisions = []);
      // One quotation per analysis revision. Pressing twice opens the one that
      // already exists rather than filing a second the client never received.
      if (list.some(q => q.quoteAnalysisId === qaId && q.analysisRevision === qa.revision)) return;
      let amount = o.amount;
      if (amount == null) {
        const t = quoteAnalysisTotals(qa);
        const rate = qa.taxRatePct != null ? qnum(qa.taxRatePct) : projectTaxRate(draft);
        amount = qnum(t.sell) * (1 + qnum(rate));
      }
      const revision = (list.length ? Math.max(...list.map(q => qnum(q.revision))) : 0) + 1;
      list.push({
        id: uid('qr'), revision, amount, date: todayISO(),
        // DRAFTING, not submitted. A quotation exists here well before it goes
        // out — the deck has to be arranged first — and calling it submitted
        // the moment it is created is how one gets chased that never left.
        stage: 'drafting',
        clientFile: null, clientFileUrl: null,
        internalAnalysisFile: null, internalAnalysisFileUrl: null,
        note: `From ${qa.name} Rev ${qa.revision}.`,
        isFinal: false, quoteAnalysisId: qaId, analysisRevision: qa.revision,
        deckSlides: null, deckPictures: {}, extraSpecs: {}, leadTimes: null,
        createdBy: currentUserName,
      });
      logAction(draft, `Created Quotation Rev ${revision} from "${qa.name}" Rev ${qa.revision} — ${fmtMoney(amount)}.`);
    });
  }
  // Submitting is what freezes it: the client now holds this version, so the
  // analysis behind it becomes read-only at the same moment.
  function submitQuoteRevision(projectId, qrId) {
    updateProject(projectId, draft => {
      const q = (draft.quoteRevisions || []).find(x => x.id === qrId);
      if (!q) return;
      q.stage = 'submitted';
      q.submittedDate = todayISO();
      q.submittedBy = currentUserName;
      const qa = (draft.quoteAnalyses || []).find(a => a.id === q.quoteAnalysisId);
      if (qa && qa.status === 'Draft') {
        qa.status = 'Issued'; qa.issuedDate = todayISO(); qa.issuedBy = currentUserName;
      }
      logAction(draft, `Quotation Rev ${q.revision} submitted to the client.`);
    });
  }
  function updateQuoteRevisionDeck(projectId, qrId, fields) {
    updateProject(projectId, draft => {
      const q = (draft.quoteRevisions || []).find(x => x.id === qrId);
      if (q) Object.assign(q, fields);
    });
  }
  function finalizeQuoteAnalysis(projectId, qaId, opts) {
    const o = opts || {};
    let filed = null;
    withQuoteAnalysis(projectId, qaId, (qa, draft) => {
      qa.status = 'Issued';
      qa.issuedDate = todayISO();
      qa.issuedBy = currentUserName;
      // The CONTRACT VALUE, which is what a quotation is worth — sell plus the
      // job's tax, not the sell figure. The caller normally passes the number
      // straight off the client document it just rendered; this recomputes it
      // when nobody did, rather than reading a field that does not exist.
      let amount = o.amount;
      if (amount == null) {
        const t = quoteAnalysisTotals(qa);
        const rate = qa.taxRatePct != null ? qnum(qa.taxRatePct) : projectTaxRate(draft);
        amount = qnum(t.sell) * (1 + qnum(rate));
      }
      const list = draft.quoteRevisions || (draft.quoteRevisions = []);
      // One revision per analysis revision, not one per press. Re-issuing the
      // same analysis updates the record it already filed rather than stacking
      // a second quotation the client never received.
      const existing = list.find(q => q.quoteAnalysisId === qaId && q.analysisRevision === qa.revision);
      if (existing) {
        existing.amount = amount;
        existing.date = todayISO();
        filed = existing;
      } else {
        const revision = (list.length ? Math.max(...list.map(q => qnum(q.revision))) : 0) + 1;
        const row = {
          id: uid('qr'), revision, amount, date: todayISO(),
          clientFile: null, clientFileUrl: null,
          internalAnalysisFile: null, internalAnalysisFileUrl: null,
          note: `Issued from ${qa.name} Rev ${qa.revision}.`,
          isFinal: false,
          quoteAnalysisId: qaId,
          analysisRevision: qa.revision,
          deckDocId: o.deckDocId || null,
          issuedBy: currentUserName,
        };
        list.push(row);
        filed = row;
      }
      qa.quoteRevisionId = filed.id;
    }, 'saved as a revision — it is now read-only, and filed under Quotes.');
    return filed;
  }
  // Push the job's margin across every scope. It CLEARS each scope's own rate
  // rather than copying the number onto it — null means follow, so the scopes
  // stay tied to the job and a later change to the job reaches them too.
  // Copying the figure would look identical today and stop following tomorrow.
  function applyQuoteMarginToAll(projectId, qaId) {
    withQuoteAnalysis(projectId, qaId, qa => {
      const moved = (qa.sections || []).filter(sec => sec.ratePct !== null && sec.ratePct !== undefined);
      moved.forEach(sec => { sec.ratePct = null; sec.rateBasis = null; });
      qa.__marginApplied = moved.length;
    }, null);
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      if (!qa) return;
      const n = qa.__marginApplied || 0;
      delete qa.__marginApplied;
      logAction(draft, `Quote Analysis "${qa.name}" — ${pct(qa.defaultRatePct)} margin applied to every scope${n ? ` (${n} scope${n === 1 ? '' : 's'} had a rate of their own)` : ''}.`);
    });
  }
  function reviseQuoteAnalysis(projectId, qaId) {
    let newId = null;
    updateProject(projectId, draft => {
      const qa = (draft.quoteAnalyses || []).find(q => q.id === qaId);
      if (!qa) return;
      const copy = cloneDeep(qa);
      copy.id = uid('qa'); copy.revision = (qa.revision || 1) + 1; copy.status = 'Draft';
      copy.createdDate = todayISO(); copy.preparedBy = currentUserName;
      copy.sections.forEach(sec => {
        sec.id = uid('qsec');
        sec.lines.forEach(l => { l.id = uid('qln'); });
      });
      qa.status = 'Superseded';
      draft.quoteAnalyses.unshift(copy);
      newId = copy.id;
      logAction(draft, `Quote Analysis "${qa.name}" revised to Rev ${copy.revision}.`);
    });
    return newId;
  }
  function addQuoteSection(projectId, qaId, name, kind) {
    const combined = quoteScopeIsCombined(name);
    const k = combined ? 'combined' : (kind === 'labor' ? 'labor' : 'supply');
    const suffix = k === 'combined' ? 'Supply & Install' : k === 'labor' ? 'Labor' : 'Supply';
    withQuoteAnalysis(projectId, qaId, qa => {
      qa.sections.push(makeQuoteSection({
        name: `${name} — ${suffix}`, scopeKey: name, kind: k,
        // Labour is neither shipped nor dutiable.
        ...(k === 'labor' ? { freightBasis: 'pct', freightPct: 0, dutyBasis: 'pct', dutyPct: 0 } : {}),
      }));
    }, `added ${suffix.toLowerCase()} scope "${name}".`);
  }
  function updateQuoteSection(projectId, qaId, secId, fields) {
    withQuoteAnalysis(projectId, qaId, qa => {
      const sec = qa.sections.find(x => x.id === secId);
      if (sec) Object.assign(sec, fields);
    });
  }
  // Removing a scope from a quote is recorded, the same way adding one is —
  // "the tile came out of Rev 3" is exactly the question asked later, and the
  // previous version wrote nothing at all: it stashed the name on the draft and
  // then read it back in a second pass that passed no note, so the change log
  // was silent.
  function removeQuoteSection(projectId, qaId, secId) {
    let note = '';
    withQuoteAnalysis(projectId, qaId, qa => {
      const sec = qa.sections.find(x => x.id === secId);
      if (!sec) return;
      note = `removed scope "${sec.name}" and its ${sec.lines.length} line${sec.lines.length === 1 ? '' : 's'}.`;
      qa.sections = qa.sections.filter(x => x.id !== secId);
    });
    if (note) withQuoteAnalysis(projectId, qaId, () => {}, note);
  }
  function addQuoteLine(projectId, qaId, secId, data) {
    withQuoteAnalysis(projectId, qaId, qa => {
      const sec = qa.sections.find(x => x.id === secId);
      if (sec) sec.lines.push(makeQuoteLine({ uom: sec.uom || '', ...(data || {}) }));
    });
  }
  function updateQuoteLine(projectId, qaId, secId, lineId, fields) {
    withQuoteAnalysis(projectId, qaId, qa => {
      const sec = qa.sections.find(x => x.id === secId);
      const line = sec && sec.lines.find(l => l.id === lineId);
      if (line) Object.assign(line, fields);
    });
  }
  function removeQuoteLine(projectId, qaId, secId, lineId) {
    withQuoteAnalysis(projectId, qaId, qa => {
      const sec = qa.sections.find(x => x.id === secId);
      if (sec) sec.lines = sec.lines.filter(l => l.id !== lineId);
    });
  }

  // Renders are organized by named set, each with its own revision history —
  // a set's scope is optional (§ renders request), unlike every other
  // scope-nested record in this app.
  function addRenderSet(projectId, data) {
    updateProject(projectId, draft => {
      const scope = data.scopeId ? findScope(draft, data.scopeId) : null;
      draft.renderSets.push({
        id: uid('rset'), scopeId: data.scopeId || null, name: data.name,
        createdBy: currentUserName, createdDate: todayISO(),
        revisions: [{ id: uid('rrev'), revisionNumber: 1, date: data.date || todayISO(), note: data.note || '', sharedWithClient: !!data.sharedWithClient, images: data.images || [] }],
      });
      logAction(draft, `Added render set "${data.name}" (Rev 1)${scope ? ` to ${scope.name}` : ''}.`);
    });
  }
  function addRenderSetRevision(projectId, setId, data) {
    updateProject(projectId, draft => {
      const set = draft.renderSets.find(x => x.id === setId);
      const revisionNumber = (set.revisions.length ? Math.max(...set.revisions.map(r => r.revisionNumber)) : 0) + 1;
      set.revisions.push({ id: uid('rrev'), revisionNumber, date: data.date || todayISO(), note: data.note || '', sharedWithClient: !!data.sharedWithClient, images: data.images || [] });
      logAction(draft, `Added Rev ${revisionNumber} to render set "${set.name}".`);
    });
  }
  function removeRenderSet(projectId, setId) {
    updateProject(projectId, draft => {
      const s = draft.renderSets.find(x => x.id === setId);
      draft.renderSets = draft.renderSets.filter(x => x.id !== setId);
      if (s) logAction(draft, `Removed render set "${s.name}".`);
    });
  }

  // ---- Company document library (shared across all projects/employees) ----
  function addLibraryDocument(data) {
    setDocumentLibrary(prev => [...prev, { id: uid('lib'), uploadedBy: currentUserName, date: todayISO(), ...data }]);
  }
  function removeLibraryDocument(docId) {
    setDocumentLibrary(prev => prev.filter(d => d.id !== docId));
  }
  function updateLibraryDocument(docId, fields) {
    setDocumentLibrary(prev => prev.map(d => d.id === docId ? { ...d, ...fields } : d));
  }

  function addQuoteRevision(projectId, data) {
    updateProject(projectId, draft => {
      const revision = (draft.quoteRevisions.length ? Math.max(...draft.quoteRevisions.map(q => q.revision)) : 0) + 1;
      draft.quoteRevisions.push({ id: uid('qr'), revision, ...data });
      logAction(draft, `Added Quote Revision ${revision} — ${fmtMoney(data.amount)}.`);
    });
    suggestStage(projectId, { stageKey: 'quote_prep', because: `Quote Revision ${(projects.find(p => p.id === projectId) || { quoteRevisions: [] }).quoteRevisions.length + 1} was uploaded` });
  }
  function updateQuoteRevision(projectId, qrId, fields) {
    updateProject(projectId, draft => {
      const q = draft.quoteRevisions.find(x => x.id === qrId);
      Object.assign(q, fields);
      const isFileOnly = Object.keys(fields).every(k => ['clientFile', 'clientFileUrl', 'internalAnalysisFile', 'internalAnalysisFileUrl'].includes(k));
      logAction(draft, isFileOnly ? `Attached file to Quote Revision ${q.revision}.` : `Edited Quote Revision ${q.revision}.`);
    });
  }
  // Marks one revision as the final/awarded quotation (only one at a time)
  // and carries its number into the Contract tab as the Original Contract
  // Value — "changing the quote to contract... transferred to the Contract
  // tab with its information."
  function convertQuoteToContract(projectId, qrId) {
    updateProject(projectId, draft => {
      draft.quoteRevisions.forEach(q => { q.isFinal = q.id === qrId; });
      const q = draft.quoteRevisions.find(x => x.id === qrId);
      draft.originalContractValue = q.amount;
      logAction(draft, `Selected Quote Revision ${q.revision} as the final quotation and converted it to the Contract (Original Contract Value set to ${fmtMoney(q.amount)}).`);
    });
  }
  function undoConvertQuoteToContract(projectId, qrId) {
    updateProject(projectId, draft => {
      const q = draft.quoteRevisions.find(x => x.id === qrId);
      if (!q || !q.isFinal) return;
      q.isFinal = false;
      draft.originalContractValue = 0;
      logAction(draft, `Undid "Convert to Contract" for Quote Revision ${q.revision}.`);
    });
  }

  // A follow-up with a next-follow-up date AND an assignee also drops a
  // Reminder onto that person's My To-Do — "next follow-ups... assigned to
  // someone and added to their to-do list" — so it surfaces somewhere the
  // assignee will actually see it, not just in this log.
  // Chasing can be paused per job — a client on holiday, a bid on hold. It is
  // a decision, so it persists and shows in its own list rather than silently
  // dropping out of the queue.
  // A copy of what went out, filed as the record. `shareItem` is for sharing
  // something WITH someone and logs a share; this is ordinary outbound
  // correspondence, so it files the mail and nothing else.
  function fileOutgoingEmail({ to, toName, subject, body, html, projectId }) {
    const n = makeNotification({
      event: 'share.received', title: subject, body: body || '',
      toUserId: null, byUser: currentUserName,
      projectId: projectId || null,
      projectName: projectId ? ((projects.find(p => p.id === projectId) || {}).name || '') : '',
    });
    const senderEmail = (currentUser && currentUser.email) || '';
    setEmailOutbox(prev => [{ ...makeQueuedEmail(n, to, senderEmail), subject, body, html }, ...prev].slice(0, 500));
    if (projectId) updateProject(projectId, draft => {
      logAction(draft, `Emailed "${subject}" to ${toName || to}.`);
    });
  }
  function setQuoteChasePaused(projectId, paused) {
    updateProject(projectId, draft => {
      draft.quoteChasePaused = !!paused;
      logAction(draft, paused ? 'Paused weekly quotation chasing.' : 'Resumed weekly quotation chasing.');
    });
  }
  function addFollowUp(projectId, data) {
    updateProject(projectId, draft => {
      draft.quotationFollowUps.push({
        id: uid('fu'), quoteRevisionId: null, sentTo: null, emailed: false,
        ...data, assigneeId: data.assigneeId || null,
      });
      logAction(draft, `Logged quotation follow-up (${data.method})${data.emailed ? ' and queued the email' : ''}.`);
    });
    if (data.nextFollowUp && data.assigneeId) {
      const project = projects.find(p => p.id === projectId);
      addPersonalItem({
        userId: data.assigneeId,
        type: 'Reminder',
        title: `Follow up — ${project ? project.name : 'Quotation'}`,
        date: data.nextFollowUp,
        projectId,
        notes: data.note || '',
        priority: 'Medium',
      });
    }
  }

  function addChangeOrder(projectId, data) {
    updateProject(projectId, draft => {
      const amount = data.type === 'Back Charge' ? -Math.abs(data.amount) : Math.abs(data.amount);
      // No number yet — assigned automatically only once approved, so the
      // sequence reflects approval order, not draft/submission order.
      // scopeId/windowRevisionId are null unless this CO came from the
      // Window Schedule's revision-impact workflow (logWindowRevision) —
      // kept on every CO regardless of origin so the shape stays uniform.
      draft.changeOrders.push({ id: uid('co'), number: null, type: data.type, amount, date: data.date, file: data.file, fileUrl: data.fileUrl || null, description: data.description, status: 'Pending', scopeId: data.scopeId || null, windowRevisionId: data.windowRevisionId || null });
      logAction(draft, `Added ${data.type} — ${fmtMoney(amount)} (Pending approval; number assigned on approval).`);
    });
  }

  function setChangeOrderStatus(projectId, coId, status) {
    updateProject(projectId, draft => {
      const co = draft.changeOrders.find(c => c.id === coId);
      co.status = status;
      if (status === 'Approved' && !co.number) {
        const prefix = co.type === 'Change Order' ? 'CO' : 'BC';
        const n = draft.changeOrders.filter(c => c.type === co.type && c.number).length + 1;
        co.number = `${prefix}-${n}`;
      }
      logAction(draft, `${co.number || co.type} status changed to ${status}.`);
    });
  }
  function updateChangeOrder(projectId, coId, fields) {
    updateProject(projectId, draft => {
      const co = draft.changeOrders.find(c => c.id === coId);
      Object.assign(co, fields);
      const isFileOnly = Object.keys(fields).every(k => ['file', 'fileUrl'].includes(k));
      logAction(draft, isFileOnly ? `Attached file to ${co.number || co.type}.` : `Edited ${co.number || co.type}.`);
    });
  }
  // Reverts a wrongly-clicked Approve/Reject back to Pending — clears the
  // assigned number too (not just the status), since the number is only
  // ever handed out in approval order (setChangeOrderStatus above); leaving
  // a stale number on an un-approved CO/BC would create a gap or, worse, a
  // collision if a different one gets approved before this one is redone.
  function undoChangeOrderApproval(projectId, coId) {
    updateProject(projectId, draft => {
      const co = draft.changeOrders.find(c => c.id === coId);
      if (!co || co.status === 'Pending') return;
      const hadNumber = co.number, previousStatus = co.status;
      co.status = 'Pending';
      co.number = null;
      logAction(draft, `Undid ${previousStatus} decision on ${hadNumber || co.type} — reverted to Pending.`);
    });
  }

  function setPaymentTermStatus(projectId, ptId, status) {
    updateProject(projectId, draft => {
      const pt = draft.paymentTerms.find(p => p.id === ptId);
      pt.status = status;
      logAction(draft, `Payment term "${pt.label}" marked ${status}.` + (status === 'Due' ? ' Accounting notified.' : ''));
    });
  }

  function addPaymentRequisition(projectId, data) {
    updateProject(projectId, draft => {
      const revision = draft.paymentRequisitions.filter(r => r.reference === data.reference).length + 1;
      const amount = data.type === 'Back Charge' ? -Math.abs(data.amount) : Math.abs(data.amount);
      draft.paymentRequisitions.push({ id: uid('req'), revision, date: data.date, amount, retainageHeld: Number(data.retainageHeld) || 0, file: data.file, fileUrl: data.fileUrl || null, status: 'Submitted', type: data.type, reference: data.reference, note: data.note, sourceApplicationId: data.sourceApplicationId || null });
      logAction(draft, `Submitted Payment Requisition (${data.type}${data.reference ? ' — ' + data.reference : ''}) — ${fmtMoney(amount)}.`);
    });
  }
  function updatePaymentRequisition(projectId, reqId, fields) {
    updateProject(projectId, draft => {
      const r = draft.paymentRequisitions.find(x => x.id === reqId);
      Object.assign(r, fields);
      logAction(draft, `Attached file to Payment Requisition R${r.revision}.`);
    });
  }

  function setPaymentRequisitionStatus(projectId, reqId, status) {
    updateProject(projectId, draft => {
      const r = draft.paymentRequisitions.find(x => x.id === reqId);
      r.status = status;
      logAction(draft, `Payment Requisition (${r.reference || r.type}) status changed to ${status}.`);
    });
  }

  async function addVendorEstimate(projectId, data) {
    const estimateNumber = await reserveEstimateNumber();
    updateProject(projectId, draft => {
      draft.vendorEstimates.push({
        id: uid('ve'), estimateNumber, vendorId: data.vendorId || null, vendorName: data.vendorName, category: data.category || 'Original Order',
        scopeId: data.scopeId || null, currency: data.currency || 'USD', status: 'Received',
        description: data.description, amount: data.amount, date: data.date,
        unplannedReason: data.unplannedReason || null, recoverability: isUnplannedCost(data.category) ? (data.recoverability || 'Pending Determination') : null,
        pmApproved: false, pmApprovedBy: null, pmApprovedDate: null,
        ownerApproved: false, ownerApprovedBy: null, ownerApprovedDate: null, poId: null,
        revisions: [{ id: uid('ver'), revision: 1, amount: data.amount, date: data.date, file: data.file || null, fileUrl: data.fileUrl || null, note: '' }],
      });
      logAction(draft, `Added estimate ${estimateNumber} — ${data.vendorName} (${data.category || 'Original Order'}, ${fmtMoney(data.amount)}).`);
    });
  }

  async function approveVendor(projectId, veId, which) {
    const project = projects.find(p => p.id === projectId);
    const ve = project && project.vendorEstimates.find(v => v.id === veId);
    const willIssue = ve && !ve.poId && (ve.pmApproved || which === 'pm') && (ve.ownerApproved || which === 'owner');
    const poNumber = willIssue ? await reservePoNumber() : undefined;
    setProjects(prev => prev.map(p => p.id === projectId ? approveVendorEstimate(p, veId, which, currentRole, currentUserName, poNumber) : p));
  }

  function setPOTermStatus(projectId, poId, termId, status) {
    updateProject(projectId, draft => {
      const po = draft.purchaseOrders.find(p => p.id === poId);
      const term = po.paymentTerms.find(t => t.id === termId);
      term.status = status;
      logAction(draft, `PO payment term "${term.label}" (${po.vendorName}) marked ${status}.`);
    });
  }
  function updatePurchaseOrder(projectId, poId, fields) {
    updateProject(projectId, draft => {
      const po = draft.purchaseOrders.find(p => p.id === poId);
      Object.assign(po, fields);
      if (fields.piFileUrl) logAction(draft, `Attached PI/PO document — ${po.vendorName}.`);
    });
  }
  // Admin/Accounting only (ctx.canEditPOPI) — the header fields here have no
  // other edit path; amount changes still go through Add Revision so that
  // audit trail stays intact.
  function updateProformaInvoice(projectId, piId, fields) {
    updateProject(projectId, draft => {
      const pi = draft.proformaInvoices.find(p => p.id === piId);
      Object.assign(pi, fields);
      logAction(draft, `Edited Proforma Invoice ${pi.piNumber}.`);
    });
  }

  function addTask(projectId, data) {
    updateProject(projectId, draft => {
      draft.tasks.push({ id: uid('task'), status: 'Open', ...data });
      logAction(draft, `Added task "${data.title}" — assigned to ${personName(teamDirectory, data.assigneeId)}.`);
    });
    notify('assignment.new', {
      toUserIds: [data.assigneeId], projectId,
      title: `New task: ${data.title}`,
      body: `${currentUserName} assigned you "${data.title}"${data.dueDate ? `, due ${data.dueDate}` : ''}.`,
      link: { view: 'project', projectId, tab: 'tasks' },
    });
  }
  function setTaskStatus(projectId, taskId, status) {
    updateProject(projectId, draft => {
      const t = draft.tasks.find(x => x.id === taskId);
      t.status = status;
      logAction(draft, `Task "${t.title}" marked ${status}.`);
    });
  }

  function addIssue(projectId, data) {
    updateProject(projectId, draft => {
      draft.issues.push({ id: uid('issue'), status: 'Open', dateRaised: todayISO(), dateResolved: null, resolution: '', resolvedBy: null, attachments: [], ...data });
      logAction(draft, `Raised issue "${data.title}" (${data.severity}).`);
    });
    notify('issue.new', {
      toUserIds: projectWatchers(projectId, [data.assigneeId]), projectId,
      title: `Issue raised: ${data.title}`,
      body: `${currentUserName} raised a ${data.severity || ''} issue on this project.`,
      link: { view: 'project', projectId, tab: 'issues' },
    });
  }
  function addIssueAttachment(projectId, issueId, name, url) {
    updateProject(projectId, draft => {
      const i = draft.issues.find(x => x.id === issueId);
      i.attachments.push({ id: uid('att'), name, url, uploadedBy: currentUserName, uploadedDate: todayISO() });
      logAction(draft, `Attached "${name}" to issue "${i.title}".`);
    });
  }
  function removeIssueAttachment(projectId, issueId, attachmentId) {
    updateProject(projectId, draft => {
      const i = draft.issues.find(x => x.id === issueId);
      i.attachments = i.attachments.filter(a => a.id !== attachmentId);
    });
  }
  // Resolving an issue requires saying HOW it was resolved — a bare status
  // flip lost that context, so this is the only path that marks an issue
  // Resolved; setIssueStatus (below) stays for reopening one.
  // A response can be an update, a hand-off, or a resolution — each with its
  // own attachments and optional follow-up. Only the 'Resolved' outcome closes
  // the issue; the other two keep it open, which is what was missing before.
  // Some mutations only know WHO to tell from inside the draft. They stage the
  // messages on draft.__notify and this drains them once the write is done —
  // notify() is a state setter and must not run inside another updater.
  function drainDraftNotices(projectId) {
    const p = projects.find(x => x.id === projectId);
    const queued = (p && p.__notify) || [];
    if (!queued.length) return;
    updateProject(projectId, draft => { delete draft.__notify; });
    queued.forEach(q => notify(q.event, { ...q, projectId, link: q.link || { view: 'project', projectId } }));
  }
  function respondToIssue(projectId, issueId, data, actingUser) {
    updateProject(projectId, draft => {
      const i = draft.issues.find(x => x.id === issueId);
      if (!i) return;
      if (!Array.isArray(i.responses)) i.responses = [];
      const outcome = data.outcome || 'Update';
      const resp = {
        id: uid('iresp'), date: todayISO(), by: actingUser, text: (data.text || '').trim(),
        outcome,
        attachments: (data.attachments || []).map(a => ({ id: uid('att'), name: a.name, url: a.url })),
        followUpAssigneeId: data.followUpAssigneeId || null,
        followUpDueDate: data.followUpDueDate || null,
      };
      i.responses.unshift(resp);
      draft.__notify = draft.__notify || [];
      // Whoever raised it, whoever was following it up, and anyone newly handed it.
      draft.__notify.push({
        event: 'issue.response',
        toUserIds: [i.assigneeId, i.raisedById, i.followUpAssigneeId].filter(Boolean),
        title: `Issue answered: ${i.title}`,
        body: `${actingUser} responded (${outcome})${resp.text ? `: ${resp.text}` : ''}.`,
      });
      if (resp.followUpAssigneeId) draft.__notify.push({
        event: 'mention',
        toUserIds: [resp.followUpAssigneeId],
        title: `Follow-up assigned: ${i.title}`,
        body: `${actingUser} asked you to follow up${resp.followUpDueDate ? ` by ${resp.followUpDueDate}` : ''}.`,
      });

      if (outcome === 'Resolved') {
        i.status = 'Resolved';
        i.dateResolved = todayISO();
        i.resolution = resp.text;     // kept in sync for every existing reader
        i.resolvedBy = actingUser;
        i.followUpAssigneeId = null;
        i.followUpDueDate = null;
        logAction(draft, `Issue "${i.title}" resolved: ${resp.text}`);
      } else {
        i.status = 'Open';
        i.dateResolved = null;
        if (outcome === 'Needs Follow-Up') {
          i.followUpAssigneeId = resp.followUpAssigneeId;
          i.followUpDueDate = resp.followUpDueDate;
          // Hand the issue itself over, so it shows up in their To-Do.
          if (resp.followUpAssigneeId) i.assigneeId = resp.followUpAssigneeId;
          logAction(draft, `Issue "${i.title}" needs follow-up${resp.followUpAssigneeId ? ` — assigned to ${personName(teamDirectory, resp.followUpAssigneeId)}` : ''}${resp.followUpDueDate ? `, due ${resp.followUpDueDate}` : ''}.`);
        } else {
          logAction(draft, `Update posted on issue "${i.title}".`);
        }
      }
      // Response attachments also join the issue's own attachment list, so
      // everything filed against the issue stays visible in one place.
      if (resp.attachments.length) {
        if (!Array.isArray(i.attachments)) i.attachments = [];
        resp.attachments.forEach(a => i.attachments.push({ ...a, uploadedBy: actingUser, uploadedDate: todayISO() }));
      }
    });
    drainDraftNotices(projectId);
  }
  function setIssueStatus(projectId, issueId, status) {
    updateProject(projectId, draft => {
      const i = draft.issues.find(x => x.id === issueId);
      i.status = status;
      if (status === 'Open') { i.dateResolved = null; i.resolution = ''; i.resolvedBy = null; }
      logAction(draft, `Issue "${i.title}" marked ${status}.`);
    });
  }
  function updateIssue(projectId, issueId, fields) {
    updateProject(projectId, draft => {
      const i = draft.issues.find(x => x.id === issueId);
      Object.assign(i, fields);
    });
  }

  // Meetings — open to any logged-in user, like the Document Library, not
  // gated by the per-module edit-rights matrix.
  // Every team attendee also gets this meeting on their own My To-Do
  // calendar — same "push a reminder to whoever it's actually for" pattern
  // as Follow-Ups and Export containers elsewhere in this app.
  function addMeeting(projectId, data) {
    const project = projects.find(p => p.id === projectId);
    const meetingId = uid('mtg');
    updateProject(projectId, draft => {
      draft.meetings.push({ id: meetingId, date: todayISO(), time: '', durationMinutes: null, title: '', notes: '', attendeeIds: [], externalAttendees: '', loggedBy: currentUserName, loggedDate: todayISO(), attachments: [], ...data });
      logAction(draft, `Logged meeting "${data.title || 'Untitled'}".`);
    });
    (data.attendeeIds || []).forEach(attendeeId => {
      addPersonalItem({
        userId: attendeeId,
        type: 'Meeting',
        title: data.title || `Meeting — ${project ? project.name : ''}`,
        date: data.date || todayISO(),
        time: data.time || '',
        durationMinutes: data.durationMinutes || null,
        projectId,
        sourceMeetingId: meetingId,
        notes: data.notes || '',
        priority: 'Medium',
      });
    });
  }
  function updateMeeting(projectId, meetingId, fields) {
    updateProject(projectId, draft => {
      const m = draft.meetings.find(x => x.id === meetingId);
      Object.assign(m, fields);
    });
  }
  function removeMeeting(projectId, meetingId) {
    updateProject(projectId, draft => { draft.meetings = draft.meetings.filter(m => m.id !== meetingId); });
  }
  function addMeetingAttachment(projectId, meetingId, name, url) {
    updateProject(projectId, draft => {
      const m = draft.meetings.find(x => x.id === meetingId);
      m.attachments.push({ id: uid('att'), name, url, uploadedBy: currentUserName, uploadedDate: todayISO() });
    });
  }
  function removeMeetingAttachment(projectId, meetingId, attachmentId) {
    updateProject(projectId, draft => {
      const m = draft.meetings.find(x => x.id === meetingId);
      m.attachments = m.attachments.filter(a => a.id !== attachmentId);
    });
  }

  // ---- Jobsite Visits ----
  // Returns the new visit's id — needed so a follow-up visit created in the
  // same submit can point back at the one that scheduled it.
  function addJobsiteVisit(projectId, data) {
    let newId = null;
    updateProject(projectId, draft => {
      const visit = makeJobsiteVisit(data, currentUserName);
      newId = visit.id;
      draft.jobsiteVisits.push(visit);
      logAction(draft, `Logged a jobsite visit${data.assigneeId ? ` — assigned to ${personName(teamDirectory, data.assigneeId)}` : ''}${data.followUpFromVisitId ? ' (follow-up)' : ''}.`);
    });
    return newId;
  }
  function updateJobsiteVisit(projectId, visitId, fields) {
    updateProject(projectId, draft => { Object.assign(draft.jobsiteVisits.find(x => x.id === visitId), fields); });
  }
  function removeJobsiteVisit(projectId, visitId) {
    updateProject(projectId, draft => { draft.jobsiteVisits = draft.jobsiteVisits.filter(x => x.id !== visitId); });
  }
  function addJobsiteVisitPicture(projectId, visitId, url) {
    updateProject(projectId, draft => {
      const v = draft.jobsiteVisits.find(x => x.id === visitId);
      v.pictures = [...v.pictures, url];
    });
  }

  // Assigns a whole department team in one action, so staffing a job is one
  // change in the log and one undo, not ten.
  function assignTeam(projectId, department, assignments) {
    updateProject(projectId, draft => {
      if (!draft.teams[department]) draft.teams[department] = {};
      const changed = [];
      Object.keys(assignments).forEach(role => {
        const next = assignments[role] || null;
        if ((draft.teams[department][role] || null) === next) return;
        draft.teams[department][role] = next;
        changed.push(role);
        if (next) {
          scopesInDepartment(draft, department, scopeLibrary).forEach(scope => {
            scope.stages.forEach(st => {
              if (!st.assignedUserId && st.responsibleRole === role) st.assignedUserId = next;
            });
          });
        }
      });
      if (changed.length) logAction(draft, `Assigned ${department} team — ${changed.length} role${changed.length === 1 ? '' : 's'} updated (${changed.join(', ')}).`);
    });
  }
  // department names which of the job's two teams (Windows / Interiors) is
  // being staffed — a job carries one team per department.
  function reassignTeamRole(projectId, department, teamRole, personId) {
    updateProject(projectId, draft => {
      if (!draft.teams[department]) draft.teams[department] = {};
      draft.teams[department][teamRole] = personId;
      // Mirror into Scopes & Schedule — fills in any stage mapped to this
      // team role that nobody has explicitly assigned yet (never overwrites
      // a manual per-stage pick made via ScopeBlock).
      if (personId) {
        // Only this department's scopes — staffing the Windows team must not
        // reach into Interiors stage assignments.
        scopesInDepartment(draft, department, scopeLibrary).forEach(scope => {
          scope.stages.forEach(st => {
            if (!st.assignedUserId && st.responsibleRole === teamRole) st.assignedUserId = personId;
          });
        });
      }
      logAction(draft, `Reassigned ${department} "${teamRole}" to ${personName(teamDirectory, personId)}.`);
    });
  }

  function updateNotes(projectId, text) {
    updateProject(projectId, draft => { draft.notes = text; });
  }
  function addManualLog(projectId, text) {
    updateProject(projectId, draft => { logAction(draft, text); });
  }
  // Fills a whole fixed contact role from one record — used when picking an
  // existing contact out of the account's Contact Log, so the seven fixed
  // roles are populated from the shared list rather than retyped per project.
  function setProjectContact(projectId, roleKey, data) {
    updateProject(projectId, draft => {
      draft.contacts[roleKey] = {
        company: data.company || '', person: data.person || '', title: data.title || '',
        phone: data.phone || '', mobile: data.mobile || '', email: data.email || '',
        preferredContactMethod: data.preferredContactMethod || '', notes: data.notes || '',
        accountContactId: data.accountContactId || null,
      };
      logAction(draft, data.person ? `Set ${roleKey} to ${data.person}.` : `Cleared ${roleKey}.`);
    });
  }
  function updateContact(projectId, roleKey, field, value) {
    updateProject(projectId, draft => {
      if (!draft.contacts[roleKey]) draft.contacts[roleKey] = { company: '', person: '', title: '', phone: '', mobile: '', email: '', preferredContactMethod: '', notes: '' };
      draft.contacts[roleKey][field] = value;
    });
  }
  // Additional Contacts is an open-ended list (not a fixed one-slot role) so
  // a project can have as many extra contacts as it actually needs.
  function addAdditionalContact(projectId, data) {
    updateProject(projectId, draft => {
      draft.additionalContacts.push({
        id: uid('contact'), label: data.label || 'Additional Contact', company: data.company || '', person: data.person || '',
        title: data.title || '', phone: data.phone || '', mobile: data.mobile || '', email: data.email || '',
        preferredContactMethod: data.preferredContactMethod || '', notes: data.notes || '',
      });
    });
  }
  function updateAdditionalContact(projectId, contactId, field, value) {
    updateProject(projectId, draft => {
      const c = draft.additionalContacts.find(x => x.id === contactId);
      if (c) c[field] = value;
    });
  }
  function removeAdditionalContact(projectId, contactId) {
    updateProject(projectId, draft => {
      draft.additionalContacts = draft.additionalContacts.filter(x => x.id !== contactId);
    });
  }

  // ---- Project information fields (address/type/size/labor/department) ----
  function updateProjectInfo(projectId, fields) {
    updateProject(projectId, draft => { Object.assign(draft, fields); });
  }
  // Admin-only project management — moving a project to another Account
  // (rare, corrects a data-entry mistake rather than a normal workflow
  // action) and deleting a project outright. Both logged, unlike the
  // routine field edits above, since they affect the account's own
  // project/financial rollups and (for delete) can't be undone from here.
  function moveProjectToAccount(projectId, newAccountId) {
    const newAccount = accounts.find(a => a.id === newAccountId);
    updateProject(projectId, draft => {
      const oldAccount = accounts.find(a => a.id === draft.accountId);
      draft.accountId = newAccountId;
      logAction(draft, `Moved to Account "${newAccount ? newAccount.name : newAccountId}" (was "${oldAccount ? oldAccount.name : draft.accountId}").`);
    });
  }
  // Deleting a project requires two-person verification — both Admin and
  // Accounting have to sign off, not just one Admin click — since it's a
  // total, unrecoverable-from-within-the-app data loss. Admin (the only role
  // that can see the "Delete Project" button at all) requests it via a
  // type-to-confirm modal, which counts as their own approval; the project
  // only actually deletes once Accounting separately approves too.
  function requestProjectDeletion(projectId) {
    updateProject(projectId, draft => {
      draft.deleteRequest = { requestedBy: currentUserName, requestedDate: todayISO(), adminApproved: currentRole === 'Admin', accountingApproved: currentRole === 'Accounting' };
      logAction(draft, `Project deletion requested by ${currentUserName} — requires both Admin and Accounting approval.`);
    });
  }
  function cancelProjectDeletion(projectId) {
    updateProject(projectId, draft => {
      logAction(draft, 'Project deletion request cancelled.');
      draft.deleteRequest = null;
    });
  }
  function decideProjectDeletion(projectId, which) {
    const project = projects.find(p => p.id === projectId);
    if (!project || !project.deleteRequest) return;
    const req = { ...project.deleteRequest, [which === 'admin' ? 'adminApproved' : 'accountingApproved']: true };
    if (req.adminApproved && req.accountingApproved) {
      setProjects(prev => prev.filter(p => p.id !== projectId));
    } else {
      updateProject(projectId, draft => {
        draft.deleteRequest = req;
        logAction(draft, `Project deletion approved by ${currentRole} — awaiting the other approval.`);
      });
    }
  }
  function updateScopeQuantity(projectId, scopeId, quantity, unit) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      scope.quantity = quantity;
      scope.unit = unit;
    });
  }

  // ---- Custom payment terms + retainage ----
  function addPaymentTerm(projectId, data) {
    updateProject(projectId, draft => {
      draft.paymentTerms.push({ id: uid('pt'), status: 'Not Due', ...data });
      logAction(draft, `Added payment term "${data.label}" (${data.pct}%).`);
    });
  }
  function removePaymentTerm(projectId, termId) {
    updateProject(projectId, draft => {
      const t = draft.paymentTerms.find(x => x.id === termId);
      draft.paymentTerms = draft.paymentTerms.filter(x => x.id !== termId);
      if (t) logAction(draft, `Removed payment term "${t.label}".`);
    });
  }
  function updatePaymentTerm(projectId, termId, fields) {
    updateProject(projectId, draft => {
      const t = draft.paymentTerms.find(x => x.id === termId);
      Object.assign(t, fields);
      logAction(draft, `Updated payment term "${t.label}".`);
    });
  }
  function setRetainagePct(projectId, pct) {
    updateProject(projectId, draft => {
      draft.retainagePct = pct;
      logAction(draft, `Set project retainage to ${pct}%.`);
    });
  }

  // ---- Delivery ----
  // Direct "Log Delivery" entry (skips the request/approval step — logistics
  // already knows it happened) — defaults fill in the same fields
  // requestDelivery sets, so this record isn't invisible to the new
  // approval-status-filtered Requested/Scheduled/Delivered subtabs.
  async function addDelivery(projectId, data) {
    const deliveryNumber = await reserveDeliveryNumber();
    updateProject(projectId, draft => {
      const del = makeDelivery({ ...data, deliveryNumber, approvalStatus: 'Approved', deliveryAddress: data.deliveryAddress || draft.address }, currentUserName);
      del.approvedBy = currentUserName;
      del.approvedDate = todayISO();
      draft.deliveries.push(del);
      logAction(draft, `Logged delivery ${deliveryNumber} — ${data.description}.`);
    });
  }
  function removeDelivery(projectId, delId) {
    updateProject(projectId, draft => { draft.deliveries = draft.deliveries.filter(d => d.id !== delId); });
  }
  function updateDelivery(projectId, delId, fields) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === delId);
      Object.assign(d, fields);
      if (fields.slipFileUrl) logAction(draft, `Attached delivery slip — ${d.description}.`);
    });
  }
  // Anyone can request a delivery (not gated by canEdit('delivery') — the
  // request itself is intentionally open); it only becomes a real Scheduled
  // delivery once a Logistic Manager approves it.
  async function requestDelivery(projectId, data) {
    const deliveryNumber = await reserveDeliveryNumber();
    updateProject(projectId, draft => {
      const del = makeDelivery({
        ...data, deliveryNumber, approvalStatus: 'Pending Approval',
        approverId: data.approverId || teamMemberFor(draft, 'Logistic Manager') || null,
        deliveryAddress: data.deliveryAddress || draft.address,
      }, currentUserName);
      draft.deliveries.push(del);
      const what = data.description || `${(data.lines || []).length} material line(s)`;
      logAction(draft, `Requested delivery ${deliveryNumber} — ${what}.`);
    });
  }
  function approveDeliveryRequest(projectId, deliveryId) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.approvalStatus = 'Approved';
      d.approvedBy = currentUserName;
      d.approvedDate = todayISO();
      logAction(draft, `Approved delivery request ${d.deliveryNumber}.`);
    });
  }
  // Approve-with-schedule and Reschedule are the same action on the same
  // record (per the client's own description: "when he approves or
  // reschedule the job, it should pop up a window where he can fill out all
  // of the information for the delivery") — one modal, one function, called
  // from either the Requested tab (first schedule) or the Scheduled tab
  // (changing an existing schedule).
  function scheduleDelivery(projectId, deliveryId, schedule) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      const wasApproved = d.approvalStatus === 'Approved';
      d.approvalStatus = 'Approved';
      d.approvedBy = currentUserName;
      d.approvedDate = todayISO();
      d.date = schedule.date || d.date;
      d.deliveryTime = schedule.deliveryTime || '';
      d.durationMinutes = schedule.durationMinutes ? Number(schedule.durationMinutes) : null;
      d.driverId = schedule.driverId || null;
      d.helperId = schedule.helperId || null;
      d.jobsiteContact = schedule.jobsiteContact || d.jobsiteContact;
      logAction(draft, `${wasApproved ? 'Rescheduled' : 'Approved and scheduled'} delivery ${d.deliveryNumber} for ${fmtDate(d.date)}${d.deliveryTime ? ` ${d.deliveryTime}` : ''}${d.driverId ? ` — driver ${personName(teamDirectory, d.driverId)}` : ''}.`);
    });
  }
  function rejectDeliveryRequest(projectId, deliveryId, reason) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.approvalStatus = 'Rejected';
      d.approvedBy = currentUserName;
      d.approvedDate = todayISO();
      d.damageShortageNotes = reason ? `Rejected: ${reason}` : 'Rejected';
      logAction(draft, `Rejected delivery request ${d.deliveryNumber}${reason ? ` — ${reason}` : ''}.`);
    });
  }
  // The driver's in-progress capture (Delivery Driver Hub) — photos added
  // while the delivery is actively happening, independent of the final
  // proof-of-delivery submission below.
  function addDeliveryPicture(projectId, deliveryId, url) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.deliveryPictures = [...d.deliveryPictures, url];
    });
  }
  // The Delivery Driver Hub's proof-of-delivery submission — the single
  // trigger that turns a delivery's lines (a PLAN against an allocation,
  // nothing withdrawn yet) into an actual release+delivery against that
  // allocation, for exactly the quantity the driver confirms. Partial and
  // Cancelled outcomes never touch the allocation for the undelivered
  // portion — it was never reserved-for-release in the first place, so it's
  // automatically still sitting there for the Logistic Manager to
  // reallocate or reschedule, no separate "revert" step needed.
  function completeDelivery(projectId, deliveryId, data) {
    const project = projects.find(p => p.id === projectId);
    const delivery = project && project.deliveries.find(x => x.id === deliveryId);
    if (!delivery) return;
    const deliveredLines = delivery.lines.map(l => {
      const upd = data.lines.find(x => x.id === l.id);
      return { ...l, deliveredQuantity: upd ? Number(upd.deliveredQuantity) || 0 : 0 };
    });
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.lines.forEach(l => {
        const dl = deliveredLines.find(x => x.id === l.id);
        l.deliveredQuantity = dl ? dl.deliveredQuantity : 0;
      });
      d.outcome = data.outcome;
      d.deliveryStatus = data.outcome === 'Complete' ? 'Delivered' : data.outcome === 'Partial' ? 'Partially Delivered' : 'Cancelled';
      d.cancelReason = data.outcome === 'Cancelled' ? (data.cancelReason || '') : null;
      d.driverNotes = data.driverNotes || d.driverNotes;
      if (data.clientSignatureUrl) d.clientSignatureUrl = data.clientSignatureUrl;
      if (data.receiverName) d.receiverName = data.receiverName;
      logAction(draft, `Delivery ${d.deliveryNumber} marked ${d.outcome}${data.outcome === 'Cancelled' && data.cancelReason ? ` — ${data.cancelReason}` : ''}.`);
    });
    if (data.outcome !== 'Cancelled') {
      deliveredLines.forEach(l => {
        if (l.deliveredQuantity > 0 && l.allocationId) {
          releaseAllocation(l.allocationId, l.deliveredQuantity, `Delivered — ${delivery.deliveryNumber}`);
          markAllocationDelivered(l.allocationId, l.deliveredQuantity, `Delivered — ${delivery.deliveryNumber}`);
        }
      });
    }
  }
  // Manual line items on a delivery/packing list — each tied to its own
  // scope, since one delivery can span more than one.
  function addDeliveryLine(projectId, deliveryId, line) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.lines.push({ id: uid('dline'), scopeId: line.scopeId || null, itemName: line.itemName, quantity: Number(line.quantity) || 0, unit: line.unit || 'Units', notes: line.notes || '' });
    });
  }
  function removeDeliveryLine(projectId, deliveryId, lineId) {
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.lines = d.lines.filter(l => l.id !== lineId);
    });
  }
  // Builds a packing list straight from a delivery's own manually-entered
  // lines (for a delivery that was never routed through a warehouse
  // release) — the packing list carries the delivery's own number too, so
  // the two stay cross-referable.
  async function generateDeliveryPackingList(projectId, deliveryId) {
    const project = projects.find(p => p.id === projectId);
    const delivery = project && project.deliveries.find(d => d.id === deliveryId);
    if (!delivery) return { error: 'Delivery not found.' };
    if (!delivery.lines.length) return { error: 'Add at least one line item before generating a packing list.' };
    const pl = makePackingList({
      packingListNumber: await reservePackingListNumber(), projectId, scopeId: delivery.scopeId,
      deliveryNumber: delivery.deliveryNumber,
      projectName: project.name, address: delivery.deliveryAddress || project.address || '', deliveryContact: delivery.jobsiteContact || '',
      plannedDeliveryDate: delivery.date,
      lines: delivery.lines.map(l => ({
        allocationId: null, materialId: null, itemName: l.itemName,
        sku: '', description: '', scopeName: (project.scopes.find(s => s.id === l.scopeId) || {}).name || '',
        quantity: l.quantity, unitOfMeasure: l.unit, packageInfo: '', notes: l.notes,
      })),
      notes: delivery.notes,
    }, currentUserName);
    setPackingLists(prev => [...prev, pl]);
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.packingListId = pl.id;
      logAction(draft, `Generated Packing List ${pl.packingListNumber} for delivery ${d.deliveryNumber}.`);
    });
    return { ok: true, packingList: pl };
  }

  // ---- Export (11-step workflow + freight estimates -> Freight PO) ----
  function addExportDocument(projectId, data) {
    updateProject(projectId, draft => {
      draft.exportDocuments.push({ id: uid('exp'), ...data });
      const step = EXPORT_WORKFLOW_STEPS.find(s => s.key === data.step);
      const scope = findScope(draft, data.scopeId);
      logAction(draft, `Added export document — ${data.name} (Step ${step ? step.order + ' ' + step.name : data.step}) to ${scope ? scope.name : 'project'}.`);
    });
  }
  function removeExportDocument(projectId, docId) {
    updateProject(projectId, draft => { draft.exportDocuments = draft.exportDocuments.filter(d => d.id !== docId); });
  }
  function updateExportDocument(projectId, docId, fields) {
    updateProject(projectId, draft => {
      const d = draft.exportDocuments.find(x => x.id === docId);
      Object.assign(d, fields);
      if (fields.fileUrl) logAction(draft, `Attached file to export document "${d.name}".`);
    });
  }
  // Containers are a top-level collection, not nested in any one project
  // (§ multi-project containers) — same reasoning as materialAllocations/
  // logisticsClaims, so no per-project changeLog entry here either; every
  // report/dashboard just filters the collection by project/scope when a
  // project view is needed. Each gets an auto-generated LEON Export number
  // and, if assigned, a matching Task on the Export Manager's My To-Do —
  // "assigned to the exporter manager as a my to-do item".
  async function addExportContainer(data) {
    const containerNumber = await reserveExportContainerNumber();
    const container = makeExportContainer({ ...data, containerNumber });
    setExportContainers(prev => [...prev, container]);
    if (data.assigneeId) {
      const shipmentProjects = (data.shipments || []).map(s => projects.find(p => p.id === s.projectId)).filter(Boolean);
      addPersonalItem({
        userId: data.assigneeId,
        type: 'To-Do',
        title: `Export container ${containerNumber}${shipmentProjects.length ? ` — ${shipmentProjects.map(p => p.name).join(', ')}` : ''}`,
        date: data.etd || todayISO(),
        projectId: shipmentProjects[0] ? shipmentProjects[0].id : null,
        notes: `From ${data.fromPort || '—'} to ${data.toPort || '—'}. BL: ${data.blNumber || '—'}.`,
        priority: 'Medium',
      });
    }
    return container;
  }
  function updateExportContainer(containerId, fields) {
    setExportContainers(prev => prev.map(c => (c.id === containerId ? { ...c, ...fields } : c)));
  }
  // Export -> Logistics hand-off — the container stays Export's
  // responsibility through planning/transit; only once every document is on
  // file and it's actually dispatched can Export explicitly hand it off, and
  // only after that can Logistics receive it (see receiveContainerMaterials'
  // own guard below).
  function handoffContainerToLogistics(containerId) {
    const container = exportContainers.find(c => c.id === containerId);
    if (!container) return;
    const missing = EXPORT_DOC_CHECKLIST_STEPS.filter(s => !docStepSatisfied(container.documents[s.key]));
    if (missing.length > 0 || !['In Transit', 'Arrived', 'Delivered'].includes(container.status)) return;
    setExportContainers(prev => prev.map(c => (c.id === containerId ? { ...c, responsibleParty: 'Logistics', handoffDate: todayISO(), handoffBy: currentUserName } : c)));
  }
  function setContainerDocument(containerId, stepKey, doc) {
    setExportContainers(prev => prev.map(c => {
      if (c.id !== containerId) return c;
      return { ...c, documents: { ...c.documents, [stepKey]: { ...(c.documents[stepKey] || {}), ...doc } } };
    }));
  }
  // ---- Trade Compliance & Tariffs ----------------------------------------
  // What ctx.tariffLibrary is: the imported seed with any overrides applied,
  // then the classifications added in the app. Every reader was already going
  // through ctx.tariffLibrary, so none of them changed.
  const allTariffClassifications = useMemo(() => {
    // Two imported libraries, because they are two different tariff REGIMES and
    // not one with different numbers: the US charges on FOB transaction value
    // with Section 232/301 stacked on top, Abu Dhabi charges 5% on CIF with a
    // recoverable VAT that must never reach scope cost. Merging them into one
    // list is safe because the destination country is on every record.
    const seeds = []
      .concat(typeof US_TARIFF_LIBRARY_SEED !== 'undefined' ? US_TARIFF_LIBRARY_SEED : [])
      .concat(typeof AE_TARIFF_LIBRARY_SEED !== 'undefined' ? AE_TARIFF_LIBRARY_SEED : []);
    return seeds.map(c => normalizeTariffClassification(tariffOverrides[c.id] || c))
      .concat(tariffLibrary);
  }, [tariffOverrides, tariffLibrary]);
  const isSeedTariffId = id => typeof id === 'string'
    && (id.indexOf('ustar-') === 0 || id.indexOf('aetar-') === 0);
  // One writer, so a seed classification and a hand-added one are edited by the
  // same code — only the destination differs.
  function writeTariffClassification(classificationId, updater) {
    if (isSeedTariffId(classificationId)) {
      setTariffOverrides(prev => {
        const base = prev[classificationId]
          || allTariffClassifications.find(c => c.id === classificationId);
        return base ? { ...prev, [classificationId]: updater(base) } : prev;
      });
    } else {
      setTariffLibrary(prev => prev.map(c => (c.id === classificationId ? updater(c) : c)));
    }
  }
  // Both collections are top-level (not per-project), same reasoning as
  // materialAllocations/logisticsClaims: a classification is a shared rate
  // reference, and a tariff line spans Project→Scope→Vendor→Export.
  function addTariffClassification(data) {
    const cls = makeTariffClassification(data, currentUserName);
    setTariffLibrary(prev => [...prev, cls]);
    return cls;
  }
  // Rate changes never overwrite: closes the current version's
  // effectiveUntil to the day before the new version's effectiveFrom (only
  // when it was still open-ended) and appends the new version, so a
  // tariffLine created earlier keeps resolving against the version that was
  // actually in force when it was written, not today's rate.
  function addTariffVersion(classificationId, data, reason) {
    const cls = allTariffClassifications.find(c => c.id === classificationId);
    if (!cls) return;
    const priorCurrent = currentTariffVersion(cls);
    const nextRevision = (cls.versions.length ? Math.max(...cls.versions.map(v => v.revision)) : 0) + 1;
    const newVersion = makeTariffVersion({ ...data, revision: nextRevision }, currentUserName);
    const versions = cls.versions.map(v => (priorCurrent && v.id === priorCurrent.id && !v.effectiveUntil) ? { ...v, effectiveUntil: addDays(newVersion.effectiveFrom, -1) } : v);
    versions.push(newVersion);
    const changeEntry = makeTariffChangeEntry({
      field: 'Tariff Rate', previousValue: priorCurrent ? `${tariffVersionTotalPct(priorCurrent)}%` : '—', newValue: `${newVersion.totalEstimatedDutyPct}%`,
      reason, effectiveDate: newVersion.effectiveFrom,
    }, currentUserName);
    writeTariffClassification(classificationId, c => ({ ...c, versions, changeLog: [changeEntry, ...c.changeLog] }));
  }
  function updateTariffClassification(classificationId, fields, reason) {
    writeTariffClassification(classificationId, cls => {
      const changeEntries = Object.keys(fields)
        .filter(k => cls[k] !== fields[k])
        .map(k => makeTariffChangeEntry({ field: k, previousValue: cls[k], newValue: fields[k], reason }, currentUserName));
      return { ...cls, ...fields, changeLog: [...changeEntries, ...cls.changeLog] };
    });
  }
  function addTariffLine(data) {
    const line = makeTariffLine(data, currentUserName);
    const next = [...tariffLines, line];
    setTariffLines(next);
    if (line.projectId && line.scopeId) recalcScopeTariffAllocation(line.projectId, line.scopeId, next);
    return line;
  }
  function updateTariffLine(lineId, fields, reason) {
    let touchedLine = null;
    let priorLine = null;
    const next = tariffLines.map(l => {
      if (l.id !== lineId) return l;
      priorLine = l;
      const changeEntries = Object.keys(fields)
        .filter(k => l[k] !== fields[k])
        .map(k => makeTariffChangeEntry({ field: k, previousValue: l[k], newValue: fields[k], reason }, currentUserName));
      const updated = { ...l, ...fields, changeLog: [...changeEntries, ...l.changeLog] };
      if (fields.customsValue !== undefined || fields.applicableTariffPct !== undefined) {
        updated.estimatedTariff = Math.round(updated.customsValue * updated.applicableTariffPct) / 100;
      }
      touchedLine = updated;
      return updated;
    });
    setTariffLines(next);
    if (touchedLine && touchedLine.projectId && touchedLine.scopeId) recalcScopeTariffAllocation(touchedLine.projectId, touchedLine.scopeId, next);
    // A line can be re-pointed to a different project/scope while editing —
    // recompute the scope it just LEFT too, or that scope's tariff total
    // would keep counting a line that no longer belongs to it.
    if (priorLine && priorLine.projectId && priorLine.scopeId && (priorLine.projectId !== touchedLine.projectId || priorLine.scopeId !== touchedLine.scopeId)) {
      recalcScopeTariffAllocation(priorLine.projectId, priorLine.scopeId, next);
    }
  }
  // Records the post-clearance actual customs charges — kept permanently
  // separate from estimatedTariff (never overwrites it), so Estimated vs
  // Actual variance stays analyzable per explicit instruction.
  function recordTariffActuals(lineId, actualsData, reason) {
    let touchedLine = null;
    const next = tariffLines.map(l => {
      if (l.id !== lineId) return l;
      const actual = makeTariffActuals(actualsData);
      const updated = { ...l, actual, actualTariff: actual._dutyTotal, actualFees: actual._feesTotal, status: 'Cleared' };
      const variance = tariffLineVariance(updated);
      updated.varianceAmount = variance.amount;
      updated.variancePct = variance.pct;
      const changeEntry = makeTariffChangeEntry({
        field: 'Actual Customs Charges', previousValue: l.actualTariff !== null && l.actualTariff !== undefined ? fmtMoney(l.actualTariff) : '—',
        newValue: fmtMoney(updated.actualTariff), reason, effectiveDate: actual.entryDate,
      }, currentUserName);
      updated.changeLog = [changeEntry, ...l.changeLog];
      touchedLine = updated;
      return updated;
    });
    setTariffLines(next);
    if (touchedLine && touchedLine.projectId && touchedLine.scopeId) recalcScopeTariffAllocation(touchedLine.projectId, touchedLine.scopeId, next);
  }
  // Full recompute-and-set (not a delta) from every tariffLine belonging to
  // the scope, so editing/adding/removing a line can never leave a stale
  // partial amount behind. Projected costs.tariffs sums estimatedTariff;
  // actual.tariffs sums the duty portion of cleared lines, actual.
  // dutiesCustoms sums the fee portion — mirrors the existing two-bucket
  // split in PROFIT_COST_FIELDS, and flows into profitability/margin
  // automatically since those calc functions are already field-agnostic
  // over PROFIT_COST_FIELDS (lib.jsx).
  function recalcScopeTariffAllocation(projectId, scopeId, linesArray) {
    const lines = (linesArray || tariffLines).filter(l => l.projectId === projectId && l.scopeId === scopeId);
    const estimatedTotal = lines.reduce((s, l) => s + (l.estimatedTariff || 0), 0);
    const clearedLines = lines.filter(l => l.actualTariff !== null && l.actualTariff !== undefined);
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope) return;
      scope.profitability.costs.tariffs = estimatedTotal;
      if (clearedLines.length) {
        scope.profitability.actual.tariffs = clearedLines.reduce((s, l) => s + (l.actualTariff || 0), 0);
        scope.profitability.actual.dutiesCustoms = clearedLines.reduce((s, l) => s + (l.actualFees || 0), 0);
      }
      logAction(draft, `Tariff cost allocation updated for scope "${scope.name}" from Trade Compliance (${lines.length} line${lines.length === 1 ? '' : 's'}).`);
    });
  }
  // Recomputes a scope's ACTUAL cost fields from the real system records
  // that already exist elsewhere in the app, instead of relying purely on
  // manual entry: vendor cost from that scope's Proforma Invoices, ocean/
  // domestic freight and warehousing from its linked export containers'
  // Logistics Costs, and tariffs/duties from Trade Compliance tariff lines
  // (mirrors recalcScopeTariffAllocation above, so both stay consistent).
  // Installation/overhead/other have no system source anywhere in the data
  // model, so they're deliberately left untouched — still manual-only.
  // Every field this writes remains a plain editable number input
  // afterward, so the user can overwrite any figure that doesn't match
  // reality; the next Sync just recomputes from source again.
  function syncScopeActualCostsFromSystem(projectId, scopeId) {
    const project = projects.find(p => p.id === projectId);
    const scope = project && findScope(project, scopeId);
    if (!scope) return;
    const validPiStatuses = ['Received', 'Under Review', 'Approved', 'Revision Requested', 'Approved for Payment', 'Partially Paid', 'Paid'];
    const scopePis = (project.proformaInvoices || []).filter(pi => pi.scopeId === scopeId && pi.partyType !== 'Freight' && validPiStatuses.includes(pi.status));
    const containers = containersForProjectScope(exportContainers, projectId, scopeId);
    // Only overwrite a field when a real source record actually exists for
    // it — a scope with no PIs/containers yet has nothing to compute FROM,
    // so its existing (possibly manually-entered) actual value is left
    // alone rather than being zeroed out just because nothing was found.
    const updates = {};
    if (scopePis.length) updates.vendorCost = scopePis.reduce((s, pi) => s + (pi.amount || 0), 0);
    if (containers.length) {
      updates.oceanFreight = containers.reduce((s, c) => s + ((c.costs && c.costs.oceanFreight) || 0) + ((c.costs && c.costs.airFreight) || 0), 0);
      updates.domesticFreight = containers.reduce((s, c) => s + ((c.costs && c.costs.inlandTrucking) || 0) + ((c.costs && c.costs.drayage) || 0), 0);
      updates.warehousing = containers.reduce((s, c) => s + ((c.costs && c.costs.warehouseHandling) || 0) + ((c.costs && c.costs.storage) || 0), 0);
    }
    if (Object.keys(updates).length === 0) return;
    updateProject(projectId, draft => {
      const s = findScope(draft, scopeId);
      if (!s) return;
      Object.assign(s.profitability.actual, updates);
      logAction(draft, `Synced actual costs for scope "${s.name}" from system records (${Object.keys(updates).join(', ')}).`);
    });
    // Tariffs/duties are Trade Compliance's own field — recompute through
    // the same function it already uses, rather than a second formula here.
    // Only do so when this scope actually has tariff lines: recalc always
    // fully overwrites projected costs.tariffs from scratch, so calling it
    // with zero matching lines would wrongly zero out a manually-entered
    // projected tariff figure for a scope Trade Compliance has never touched.
    const scopeTariffLines = tariffLines.filter(l => l.projectId === projectId && l.scopeId === scopeId);
    if (scopeTariffLines.length) recalcScopeTariffAllocation(projectId, scopeId);
  }
  // Logistics Claims — top-level (not per-project) since a claim can span a
  // shipment involving one project but be filed against a company-wide
  // vendor/carrier relationship; every report/dashboard just filters by
  // projectId when a project view is needed, same as materialAllocations.
  async function addLogisticsClaim(data) {
    const claimNumber = await reserveClaimNumber();
    const claim = makeLogisticsClaim({ ...data, claimNumber }, currentUserName);
    setLogisticsClaims(prev => [...prev, claim]);
    return claim;
  }
  function updateLogisticsClaim(claimId, fields) {
    setLogisticsClaims(prev => prev.map(c => (c.id === claimId ? { ...c, ...fields } : c)));
  }
  function setLogisticsClaimStatus(claimId, status) {
    updateLogisticsClaim(claimId, { status });
  }
  async function addFreightEstimate(projectId, data) {
    const estimateNumber = await reserveEstimateNumber();
    updateProject(projectId, draft => {
      draft.freightEstimates.push({
        id: uid('fre'), estimateNumber, scopeId: data.scopeId || null, forwarderId: data.forwarderId || null, carrier: data.carrier, category: data.category || 'Original Order',
        currency: data.currency || 'USD', status: 'Received',
        description: data.description, amount: data.amount, date: data.date,
        exportApproved: false, exportApprovedBy: null, exportApprovedDate: null,
        adminApproved: false, adminApprovedBy: null, adminApprovedDate: null, poId: null,
        revisions: [{ id: uid('ver'), revision: 1, amount: data.amount, date: data.date, file: data.file || null, fileUrl: data.fileUrl || null, note: '' }],
      });
      logAction(draft, `Added estimate ${estimateNumber} — freight ${data.carrier} (${data.category || 'Original Order'}, ${fmtMoney(data.amount)}).`);
    });
  }
  function updateFreightEstimate(projectId, freId, fields) {
    updateProject(projectId, draft => {
      const fre = draft.freightEstimates.find(x => x.id === freId);
      Object.assign(fre, fields);
      if (fields.fileUrl) logAction(draft, `Attached estimate document — ${fre.carrier}.`);
    });
  }
  function addFreightEstimateRevision(projectId, freId, data) {
    updateProject(projectId, draft => {
      const fre = draft.freightEstimates.find(x => x.id === freId);
      const revision = (fre.revisions.length ? Math.max(...fre.revisions.map(r => r.revision)) : 0) + 1;
      fre.revisions.push(makeRevisionEntry({ revisionNumber: revision, date: data.date, reasonForRevision: data.reasonForRevision, previousAmount: fre.amount, revisedAmount: data.amount, notes: data.note, file: data.file, fileUrl: data.fileUrl }, currentUserName));
      fre.amount = data.amount;
      logAction(draft, `Added revision ${revision} to estimate ${fre.estimateNumber || fre.carrier} (${fmtMoney(data.amount)}).`);
    });
  }
  function updateFreightEstimateRevision(projectId, freId, revId, fields) {
    updateProject(projectId, draft => {
      const fre = draft.freightEstimates.find(x => x.id === freId);
      const r = fre.revisions.find(x => x.id === revId);
      Object.assign(r, fields);
      if (fields.fileUrl) logAction(draft, `Attached quotation file to ${fre.carrier} Rev ${r.revision}.`);
    });
  }
  function updateFreightPO(projectId, poId, fields) {
    updateProject(projectId, draft => {
      const po = draft.freightPOs.find(x => x.id === poId);
      Object.assign(po, fields);
      if (fields.piFileUrl) logAction(draft, `Attached PI document — ${po.carrier} freight PO.`);
    });
  }

  // ---- Purchase Order revision history (§ procurement chain request) —
  // a PO was previously mutated in place with no history at all; every
  // change from here on appends instead. ----
  function addPurchaseOrderRevision(projectId, poId, data) {
    updateProject(projectId, draft => {
      const po = draft.purchaseOrders.find(x => x.id === poId);
      const revision = (po.revisions.length ? Math.max(...po.revisions.map(r => r.revisionNumber)) : 0) + 1;
      po.revisions.push(makeRevisionEntry({ revisionNumber: revision, date: data.date, reasonForRevision: data.reasonForRevision, previousAmount: po.amount, revisedAmount: data.amount, notes: data.notes, file: data.file, fileUrl: data.fileUrl }, currentUserName));
      po.amount = data.amount;
      po.status = 'Revised';
      logAction(draft, `Added revision ${revision} to PO ${po.poNumber} (${fmtMoney(data.amount)}).`);
    });
  }
  function addFreightPORevision(projectId, poId, data) {
    updateProject(projectId, draft => {
      const po = draft.freightPOs.find(x => x.id === poId);
      const revision = (po.revisions.length ? Math.max(...po.revisions.map(r => r.revisionNumber)) : 0) + 1;
      po.revisions.push(makeRevisionEntry({ revisionNumber: revision, date: data.date, reasonForRevision: data.reasonForRevision, previousAmount: po.amount, revisedAmount: data.amount, notes: data.notes, file: data.file, fileUrl: data.fileUrl }, currentUserName));
      po.amount = data.amount;
      po.status = 'Revised';
      logAction(draft, `Added revision ${revision} to freight PO ${po.poNumber} (${fmtMoney(data.amount)}).`);
    });
  }
  function setPurchaseOrderStatus(projectId, poId, status, isFreight) {
    updateProject(projectId, draft => {
      const po = (isFreight ? draft.freightPOs : draft.purchaseOrders).find(x => x.id === poId);
      po.status = status;
      logAction(draft, `${isFreight ? 'Freight PO' : 'PO'} ${po.poNumber} marked ${status}.`);
    });
  }

  // ---- Proforma Invoice (§ procurement chain request) — converts an
  // approved PO (vendor or freight) into a PI, one shared array distinguished
  // by partyType, so nothing is written twice. ----
  async function convertPOToPI(projectId, partyType, poId, data) {
    const piNumber = await reservePiNumber();
    updateProject(projectId, draft => {
      const poList = partyType === 'Freight' ? draft.freightPOs : draft.purchaseOrders;
      const po = poList.find(x => x.id === poId);
      po.status = 'Converted to PI';
      const pi = makeProformaInvoice({
        piNumber, partyType, poId: po.id, estimateId: po.vendorEstimateId || po.freightEstimateId || null,
        vendorId: po.vendorId || null, vendorName: po.vendorName || po.carrier, projectId, scopeId: po.scopeId || null,
        piDate: data.piDate, currency: data.currency || po.currency, amount: data.amount,
        paymentTerms: data.paymentTerms, depositRequirement: data.depositRequirement, balanceRequirement: data.balanceRequirement,
        freight: data.freight, taxes: data.taxes, duties: data.duties, otherCharges: data.otherCharges,
        file: data.file, fileUrl: data.fileUrl, notes: data.notes, materialLines: data.materialLines || [],
      }, currentUserName);
      draft.proformaInvoices.push(pi);
      const variancePct = po.amount ? ((data.amount - po.amount) / po.amount) * 100 : 0;
      logAction(draft, `Converted PO ${po.poNumber} to Proforma Invoice ${piNumber} (${fmtMoney(Number(data.amount) || 0)}${Math.abs(variancePct) >= 1 ? `, ${variancePct > 0 ? '+' : ''}${variancePct.toFixed(1)}% vs PO` : ''}).`);
    });
  }
  function updatePiMaterialLines(projectId, piId, materialLines) {
    updateProject(projectId, draft => {
      const pi = draft.proformaInvoices.find(x => x.id === piId);
      pi.materialLines = materialLines;
      logAction(draft, `Updated material list on PI ${pi.piNumber} (${materialLines.length} line${materialLines.length === 1 ? '' : 's'}).`);
    });
  }
  function addProformaInvoiceRevision(projectId, piId, data) {
    updateProject(projectId, draft => {
      const pi = draft.proformaInvoices.find(x => x.id === piId);
      const revision = (pi.revisions.length ? Math.max(...pi.revisions.map(r => r.revisionNumber)) : 0) + 1;
      pi.revisions.push(makeRevisionEntry({ revisionNumber: revision, date: data.date, reasonForRevision: data.reasonForRevision, previousAmount: pi.amount, revisedAmount: data.amount, notes: data.notes, file: data.file, fileUrl: data.fileUrl }, currentUserName));
      pi.amount = data.amount;
      logAction(draft, `Added revision ${revision} to PI ${pi.piNumber} (${fmtMoney(data.amount)}).`);
    });
  }
  function setPIStatus(projectId, piId, status) {
    updateProject(projectId, draft => {
      const pi = draft.proformaInvoices.find(x => x.id === piId);
      pi.status = status;
      logAction(draft, `PI ${pi.piNumber} marked ${status}.`);
    });
  }
  // PI variance check — used both to gate the "Approve for Payment" action
  // in the UI and to double-check server-side (well, client-side, but the
  // same rule) before actually approving.
  function piExceedsApprovedPO(project, pi) {
    const poList = pi.partyType === 'Freight' ? project.freightPOs : project.purchaseOrders;
    const po = poList.find(x => x.id === pi.poId);
    if (!po || !po.amount) return { po: null, overThreshold: false, diffAmount: 0, diffPct: 0 };
    const diffAmount = pi.amount - po.amount;
    const diffPct = (diffAmount / po.amount) * 100;
    return { po, overThreshold: diffAmount > po.amount * PI_VARIANCE_REVIEW_THRESHOLD, diffAmount, diffPct };
  }
  function approvePIForPayment(projectId, piId) {
    const project = projects.find(p => p.id === projectId);
    const pi = project && project.proformaInvoices.find(x => x.id === piId);
    if (!pi) return;
    const { overThreshold } = piExceedsApprovedPO(project, pi);
    if (overThreshold && !canApproveInvoices(currentRole)) return;
    updateProject(projectId, draft => {
      const draftPi = draft.proformaInvoices.find(x => x.id === piId);
      const draftPoList = draftPi.partyType === 'Freight' ? draft.freightPOs : draft.purchaseOrders;
      const draftPo = draftPoList.find(x => x.id === draftPi.poId);
      draftPi.status = 'Approved for Payment';
      const inv = makeApInvoice({
        partyType: draftPi.partyType, vendorId: draftPi.vendorId, vendorName: draftPi.vendorName,
        projectId, scopeId: draftPi.scopeId, invoiceNumber: draftPi.piNumber, invoiceDate: draftPi.piDate,
        dueDate: draftPi.piDate, amount: draftPi.amount, currency: draftPi.currency,
        poReference: draftPo ? draftPo.poNumber : '', description: `Proforma Invoice ${draftPi.piNumber}`,
      }, currentUserName);
      draft.apInvoices.push(inv);
      draftPi.apInvoiceId = inv.id;
      logAction(draft, `PI ${draftPi.piNumber} approved for payment — AP invoice ${inv.invoiceNumber} auto-created (${fmtMoney(draftPi.amount)}).`);
    });
  }
  async function approveFreight(projectId, freId, which) {
    const project = projects.find(p => p.id === projectId);
    const fre = project && project.freightEstimates.find(f => f.id === freId);
    const willIssue = fre && !fre.poId && (fre.exportApproved || which === 'export') && (fre.adminApproved || which === 'admin');
    const poNumber = willIssue ? await reservePoNumber() : undefined;
    setProjects(prev => prev.map(p => p.id === projectId ? approveFreightEstimate(p, freId, which, currentRole, currentUserName, poNumber) : p));
  }

  // ---- Installation / Field Ops ----
  function addInstallationRecord(projectId, data) {
    updateProject(projectId, draft => {
      draft.installationRecords.push({
        id: uid('inst'), status: 'Not Ready', pctComplete: 0, photos: [], qcStatus: 'Not Started', signOff: null, actualStart: null, actualCompletion: null, notes: '', assignedSubcontractorId: null,
        approvalStatus: 'Pending Installer Approval', rescheduleRequest: null, scheduledTime: '', durationMinutes: '', outcome: null, completionNotes: '', returnVisit: null,
        ...data,
      });
      logAction(draft, `Added installation record — ${data.building || ''} ${data.floor || ''} ${data.unit || ''} ${data.room || ''}.`.replace(/\s+/g, ' ').trim());
    });
  }
  function updateInstallationRecord(projectId, recId, fields) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      Object.assign(rec, fields);
    });
  }
  function setInstallationStatus(projectId, recId, status) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      rec.status = status;
      const today = todayISO();
      if (status === 'Installation Started' && !rec.actualStart) rec.actualStart = today;
      if (status === 'Installation Complete') {
        if (!rec.actualCompletion) rec.actualCompletion = today;
        rec.pctComplete = 100;
        rec.qcStatus = 'Required';
      }
      if (status === 'Approved/Closed') rec.qcStatus = 'Approved';
      logAction(draft, `Installation record (${rec.room || rec.unit || rec.id}) status changed to "${status}".`);
    });
  }
  // Shared by the PC's initial "Schedule & Assign" and a later "Reschedule"
  // (same window for both, mirroring the Delivery pipeline's Approve/
  // Reschedule pattern) — either way it (re)opens the installer's approval
  // gate, clearing any pending reschedule request of their own.
  function rescheduleInstallation(projectId, recId, fields) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      Object.assign(rec, fields, { approvalStatus: 'Pending Installer Approval', rescheduleRequest: null });
      logAction(draft, `Installation record (${rec.room || rec.unit || rec.id}) scheduled for ${fmtDate(rec.scheduledStart)}${rec.scheduledTime ? ` ${rec.scheduledTime}` : ''} — awaiting installer approval.`);
    });
  }
  function approveInstallationSchedule(projectId, recId) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      rec.approvalStatus = 'Approved';
      rec.rescheduleRequest = null;
      logAction(draft, `Installation schedule approved by the installer (${rec.room || rec.unit || rec.id}).`);
    });
  }
  function requestInstallationReschedule(projectId, recId, data) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      rec.approvalStatus = 'Reschedule Requested';
      rec.rescheduleRequest = { proposedDate: data.proposedDate || null, proposedTime: data.proposedTime || '', reason: data.reason || '', requestedDate: todayISO() };
      logAction(draft, `Installer requested a reschedule for installation record (${rec.room || rec.unit || rec.id}).`);
    });
  }
  // Installer's on-site outcome — done/partial with notes/photos, and if
  // partial, their own return visit (date/time/duration), same shape as the
  // Delivery outcome flow. A Complete outcome also advances the overall
  // lifecycle status, reusing setInstallationStatus's own side effects.
  function logInstallationCompletion(projectId, recId, data) {
    updateProject(projectId, draft => {
      const rec = draft.installationRecords.find(r => r.id === recId);
      rec.outcome = data.outcome;
      rec.completionNotes = data.completionNotes || '';
      if (data.pctComplete !== undefined && data.pctComplete !== null) rec.pctComplete = Number(data.pctComplete) || 0;
      if (data.photos && data.photos.length) rec.photos = [...rec.photos, ...data.photos];
      rec.returnVisit = data.outcome === 'Partial' ? (data.returnVisit || null) : null;
      const today = todayISO();
      if (data.outcome === 'Complete') {
        rec.status = 'Installation Complete';
        if (!rec.actualCompletion) rec.actualCompletion = today;
        rec.pctComplete = 100;
        rec.qcStatus = 'Required';
      } else {
        if (!rec.actualStart) rec.actualStart = today;
        if (rec.status === 'Not Ready' || rec.status === 'Ready') rec.status = 'Installation Started';
      }
      logAction(draft, `Installation completion logged (${rec.room || rec.unit || rec.id}) — ${data.outcome}${data.outcome === 'Partial' && data.returnVisit ? `, return visit ${fmtDate(data.returnVisit.date)}` : ''}.`);
    });
  }
  function addDailyFieldReport(projectId, data) {
    updateProject(projectId, draft => {
      const manHours = (Number(data.crewCount) || 0) * (Number(data.hoursWorked) || 0);
      draft.dailyFieldReports.push({ id: uid('dfr'), manHours, ...data });
      logAction(draft, `Submitted Daily Field Report for ${fmtDate(data.date)} — ${data.crewCount} crew, ${manHours} man-hours.`);
    });
  }
  function addFieldIssue(projectId, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, data.scopeId);
      const seq = (draft.fieldIssues.length || 0) + 1;
      const issueNumber = generateFieldIssueNumber(draft, scope, seq);
      draft.fieldIssues.push({ id: uid('fis'), issueNumber, status: 'Open', dateRaised: todayISO(), ...data });
      logAction(draft, `Reported field issue ${issueNumber} — ${data.issueType} (${data.urgency}).${data.workStopped ? ' WORK STOPPED.' : ''}`);
    });
  }
  function setFieldIssueStatus(projectId, issueId, status) {
    updateProject(projectId, draft => {
      const i = draft.fieldIssues.find(x => x.id === issueId);
      i.status = status;
      logAction(draft, `Field issue ${i.issueNumber} marked ${status}.`);
    });
  }
  function addMaterialReceipt(projectId, data) {
    updateProject(projectId, draft => {
      draft.materialReceipts.push({ id: uid('mr'), date: todayISO(), photos: [], ...data });
      logAction(draft, `Logged material receiving — ${data.description} (${data.outcome}).`);
    });
  }
  function addPunchItem(projectId, data) {
    updateProject(projectId, draft => {
      draft.punchItems.push({
        id: uid('punch'), status: 'Open', dateReported: todayISO(), photoAfter: null, completedBy: null, completionDate: null, qcApproval: null,
        responseStatus: null, responseNotes: '', responsePhotos: [], repairCompletedDate: null, respondedBy: null, respondedDate: null,
        ...data,
      });
      logAction(draft, `Added punch item — ${data.item} (${data.priority}).`);
    });
  }
  // The field-level response (§ punch response request) — Completed,
  // Pending, Additional Material Needed, or Not Done — with notes, a
  // repair-completed date, and photos of the finished item. New photos are
  // always appended, never replacing ones from an earlier response. A
  // "Completed" response also advances the overall lifecycle status to
  // Awaiting Verification, same as the existing PM sign-off flow.
  function respondToPunchItem(projectId, punchId, data) {
    updateProject(projectId, draft => {
      const item = draft.punchItems.find(x => x.id === punchId);
      item.responseStatus = data.responseStatus;
      item.responseNotes = data.responseNotes || '';
      item.repairCompletedDate = data.responseStatus === 'Completed' ? (data.repairCompletedDate || todayISO()) : (data.repairCompletedDate || item.repairCompletedDate);
      item.respondedBy = currentUserName;
      item.respondedDate = todayISO();
      if (data.newPhotos && data.newPhotos.length) item.responsePhotos = [...item.responsePhotos, ...data.newPhotos];
      if (data.responseStatus === 'Completed' && item.status !== 'Closed') {
        item.status = 'Completed – Awaiting Verification';
        item.completedBy = currentUserName;
        item.completionDate = item.repairCompletedDate;
      } else if (item.status !== 'Closed') {
        item.status = 'Open';
      }
      logAction(draft, `Punch item "${item.item}" — response: ${data.responseStatus}.`);
    });
  }
  function setPunchStatus(projectId, punchId, status) {
    updateProject(projectId, draft => {
      const item = draft.punchItems.find(x => x.id === punchId);
      // Two-step closure: field roles may only move to "Completed – Awaiting
      // Verification"; only a PM-tier role can close it out.
      const canClose = ['Admin', 'Accounting', 'General Manager', 'Project Coordinator'].includes(currentRole);
      if (status === 'Closed' && !canClose) return;
      item.status = status;
      if (status === 'Completed – Awaiting Verification') { item.completedBy = currentUserName; item.completionDate = todayISO(); }
      if (status === 'Closed') item.qcApproval = currentUserName;
      logAction(draft, `Punch item "${item.item}" marked ${status}.`);
    });
  }
  function assignPunchItem(projectId, punchId, subcontractorId) {
    updateProject(projectId, draft => {
      const item = draft.punchItems.find(x => x.id === punchId);
      item.assignedSubcontractorId = subcontractorId || null;
      logAction(draft, `Punch item "${item.item}" assigned to a subcontractor.`);
    });
  }
  // The subcontractor books their own return visit before responding — a
  // separate step from the response itself, so a scheduled-but-not-yet-
  // worked item is visible to the PC ahead of time.
  function schedulePunchReturn(projectId, punchId, data) {
    updateProject(projectId, draft => {
      const item = draft.punchItems.find(x => x.id === punchId);
      item.scheduledReturnDate = data.date || null;
      item.scheduledReturnTime = data.time || '';
      item.scheduledReturnDuration = data.durationMinutes || '';
      logAction(draft, `Return visit scheduled for punch item "${item.item}" — ${fmtDate(data.date)}.`);
    });
  }
  // The PC's per-item decision once a subcontractor has responded — Approved
  // reuses the existing Verify & Close step; Rejected sends it back to Open
  // and clears the scheduled return so the subcontractor has to book another
  // one before responding again.
  function decidePunchResponse(projectId, punchId, decision, note) {
    updateProject(projectId, draft => {
      const item = draft.punchItems.find(x => x.id === punchId);
      const canClose = ['Admin', 'Accounting', 'General Manager', 'Project Coordinator'].includes(currentRole);
      if (!canClose) return;
      if (decision === 'Approved') {
        item.status = 'Closed';
        item.qcApproval = currentUserName;
        item.lastRejectionNote = null;
        logAction(draft, `Punch item "${item.item}" — work approved and closed.`);
      } else {
        item.status = 'Open';
        item.scheduledReturnDate = null;
        item.scheduledReturnTime = '';
        item.scheduledReturnDuration = '';
        item.lastRejectionNote = note || '';
        logAction(draft, `Punch item "${item.item}" — work rejected, awaiting another scheduled return.`);
      }
    });
  }
  // Field Measurement "Not Applicable" toggle, per scope.
  function setFieldMeasurementNA(projectId, scopeId, value) {
    updateProject(projectId, draft => {
      draft.fieldMeasurementNA[scopeId] = value;
    });
  }

  // ---- Field Measurement Reports (installers/field team record actual site
  // conditions; every visit appends a new revision, never overwrites the last) ----
  function addFieldMeasurementThread(projectId, data) {
    updateProject(projectId, draft => {
      const thread = makeFieldMeasurementThread(data.scopeId, data.building, data.floor, data.unit, data.room, currentUserName, data.requestedById || currentUserId);
      draft.fieldMeasurements.push(thread);
      logAction(draft, `Started field measurement report for ${[data.building, data.floor, data.unit, data.room].filter(Boolean).join(' · ') || 'scope'}.`);
    });
  }
  // ---- Subcontractor invoice: sales approval gate -------------------------
  // A sub invoice sits at 'Pending Sales Approval' until the project's Sales
  // Person confirms the work. Only then does it become Accounting's problem.
  function salesApproveApInvoice(projectId, invoiceId, approved, note) {
    updateProject(projectId, draft => {
      const inv = (draft.apInvoices || []).find(i => i.id === invoiceId);
      if (!inv) return;
      if (approved) {
        inv.approvalStatus = 'Pending Approval';   // now with Accounting
        inv.salesApprovedBy = currentUserName;
        inv.salesApprovedDate = todayISO();
        inv.salesApprovalNote = note || '';
        inv.history = inv.history || [];
        inv.history.unshift(makeApHistoryEntry(currentUserName, `Sales approved${note ? ` — ${note}` : ''}. Released to Accounting.`));
        logAction(draft, `Subcontractor invoice ${inv.invoiceNumber} approved by Sales (${currentUserName}) — released to Accounting.`);
      } else {
        inv.approvalStatus = 'Revision Requested';
        inv.salesApprovalNote = note || '';
        inv.history = inv.history || [];
        inv.history.unshift(makeApHistoryEntry(currentUserName, `Sales sent back${note ? ` — ${note}` : ''}.`));
        logAction(draft, `Subcontractor invoice ${inv.invoiceNumber} sent back by Sales (${currentUserName})${note ? ` — ${note}` : ''}.`);
      }
    });
  }
  // Can this user give the sales approval on that invoice? The named approver,
  // or an Admin/GM as the fallback when that person is unavailable.
  function canSalesApprove(invoice) {
    if (!invoice || invoice.approvalStatus !== 'Pending Sales Approval') return false;
    if (['Admin', 'General Manager'].includes(currentRole)) return true;
    return !!invoice.salesApproverId && invoice.salesApproverId === effectiveUserId;
  }

  // ---- Field service close-out ------------------------------------------
  // Measurements, installations and punch items follow the same shape: the
  // office requests it, the field performs it, and the REQUESTER closes it out.
  // One pair of functions for all three, keyed by which collection it lives in,
  // so the three flows can't drift apart.
  const FIELD_COLLECTIONS = {
    measurement: { list: 'fieldMeasurements', label: 'Field measurement' },
    installation: { list: 'installationRecords', label: 'Installation' },
    punch: { list: 'punchItems', label: 'Punch item' },
  };
  function findFieldRecord(draft, kind, recordId) {
    const c = FIELD_COLLECTIONS[kind];
    return c ? (draft[c.list] || []).find(r => r.id === recordId) : null;
  }
  // Field crew / subcontractor hands the work back for approval.
  function submitFieldForCloseout(projectId, kind, recordId, note) {
    updateProject(projectId, draft => {
      const rec = findFieldRecord(draft, kind, recordId);
      if (!rec) return;
      rec.closeoutStatus = 'Submitted for Close-Out';
      rec.closeoutNote = note || '';
      logAction(draft, `${FIELD_COLLECTIONS[kind].label} submitted for close-out by ${currentUserName}.`);
    });
  }
  // The requester approves (or sends back) the work performed.
  function closeOutFieldRecord(projectId, kind, recordId, approved, note) {
    updateProject(projectId, draft => {
      const rec = findFieldRecord(draft, kind, recordId);
      if (!rec) return;
      if (approved) {
        rec.closeoutStatus = 'Closed Out';
        rec.closedOutBy = currentUserName;
        rec.closedOutDate = todayISO();
        rec.closeoutNote = note || rec.closeoutNote || '';
        logAction(draft, `${FIELD_COLLECTIONS[kind].label} closed out by ${currentUserName}${note ? ` — ${note}` : ''}.`);
      } else {
        rec.closeoutStatus = 'Revision Requested';
        rec.closeoutNote = note || '';
        logAction(draft, `${FIELD_COLLECTIONS[kind].label} sent back for revision by ${currentUserName}${note ? ` — ${note}` : ''}.`);
      }
    });
  }
  // Re-point who owns the close-out (e.g. the original requester has left).
  function setFieldRequester(projectId, kind, recordId, personId) {
    updateProject(projectId, draft => {
      const rec = findFieldRecord(draft, kind, recordId);
      if (rec) rec.requestedById = personId || null;
    });
  }

  function addFieldMeasurementRevision(projectId, threadId, data) {
    updateProject(projectId, draft => {
      const thread = draft.fieldMeasurements.find(t => t.id === threadId);
      const revisionNumber = thread.revisions.length;
      thread.revisions.push({
        id: uid('fmrev'), revisionNumber,
        measurementDate: data.measurementDate || todayISO(), siteVisitDate: data.siteVisitDate || todayISO(),
        installer: data.installer || currentUserName, status: data.status,
        unitSystem: data.unitSystem || 'Imperial',
        // Rough opening vs finished walls — see MEASUREMENT_BASIS (data.jsx).
        basis: data.basis || 'Rough Opening',
        basisNote: data.basisNote || '',
        measurements: data.measurements || [], notes: data.notes || '',
        attachments: data.attachments || [], photos: data.photos || [],
        fieldNotes: data.fieldNotes || [],
      });
      thread.status = data.status;
      logAction(draft, `Field measurement Rev.${revisionNumber} recorded — status: ${data.status}.`);
      if (data.status === 'Field Condition Requires Review') {
        logAction(draft, `⚠ FIELD CONDITION REQUIRES REVIEW — ${[thread.building, thread.floor, thread.unit, thread.room].filter(Boolean).join(' · ') || 'field measurement'} needs project/design team review before production or installation continues.`);
      }
    });
  }

  // ---- Vendors (shared directory) ----
  function addVendor(data) {
    const v = {
      ...makeVendor(data.name, data.contactPerson, data.phone, data.email, data.address, data.defaultPaymentTerms, data.notes),
      vendorType: data.vendorType || 'Material Supplier', status: data.status || 'Active',
      contactTitle: data.contactTitle || '', mobile: data.mobile || '', website: data.website || '',
      city: data.city || '', state: data.state || '', zip: data.zip || '', country: data.country || '',
    };
    setVendors(prev => [...prev, v]);
    return v;
  }
  function updateVendorTerms(vendorId, terms) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, defaultPaymentTerms: terms } : v));
  }
  function updateVendor(vendorId, fields) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, ...fields } : v));
  }
  function updateVendorBilling(vendorId, billingFields) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, billing: { ...v.billing, ...billingFields } } : v));
  }
  function addVendorContact(vendorId, contact) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, contacts: [...v.contacts, { ...makeVendorContact(contact.role), ...contact }] } : v));
  }
  function updateVendorContact(vendorId, contactId, fields) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, contacts: v.contacts.map(c => c.id === contactId ? { ...c, ...fields } : c) } : v));
  }
  function removeVendorContact(vendorId, contactId) {
    setVendors(prev => prev.map(v => v.id === vendorId ? { ...v, contacts: v.contacts.filter(c => c.id !== contactId) } : v));
  }

  // ---- Freight forwarders (shared directory, same shape as Vendors) ----
  function addFreightForwarder(data) {
    const f = {
      ...makeVendor(data.name, data.contactPerson, data.phone, data.email, data.address, data.defaultPaymentTerms, data.notes),
      vendorType: 'Freight / Logistics', status: data.status || 'Active',
      contactTitle: data.contactTitle || '', mobile: data.mobile || '', website: data.website || '',
      city: data.city || '', state: data.state || '', zip: data.zip || '', country: data.country || '',
    };
    setFreightForwarders(prev => [...prev, f]);
    return f;
  }
  function updateFreightForwarderTerms(forwarderId, terms) {
    setFreightForwarders(prev => prev.map(f => f.id === forwarderId ? { ...f, defaultPaymentTerms: terms } : f));
  }
  function updateFreightForwarder(forwarderId, fields) {
    setFreightForwarders(prev => prev.map(f => f.id === forwarderId ? { ...f, ...fields } : f));
  }
  function updateFreightForwarderBilling(forwarderId, billingFields) {
    setFreightForwarders(prev => prev.map(f => f.id === forwarderId ? { ...f, billing: { ...f.billing, ...billingFields } } : f));
  }
  function addFreightForwarderContact(forwarderId, contact) {
    setFreightForwarders(prev => prev.map(f => f.id === forwarderId ? { ...f, contacts: [...f.contacts, { ...makeVendorContact(contact.role), ...contact }] } : f));
  }

  // ---- Subcontractors (own directory — company/individual, trade, billing, portal login) ----
  function addSubcontractor(data) {
    const s = makeSubcontractor(data, currentUserName);
    setSubcontractors(prev => [...prev, s]);
    // Portal login is optional — only create a teamDirectory entry (the
    // login system's source of truth) when a username was actually given.
    if (data.username && data.username.trim()) {
      setTeamDirectory(prev => [...prev, {
        id: uid('person'), name: s.contactName || s.companyName, roles: [], securityRole: 'Subcontractor',
        email: s.email, username: data.username.trim().toLowerCase(), password: data.password || '', active: true,
        subcontractorId: s.id,
      }]);
    }
    return s;
  }
  function updateSubcontractor(subId, fields) {
    setSubcontractors(prev => prev.map(s => s.id === subId ? { ...s, ...fields } : s));
  }
  function updateSubcontractorBilling(subId, billingFields) {
    setSubcontractors(prev => prev.map(s => s.id === subId ? { ...s, billing: { ...s.billing, ...billingFields } } : s));
  }
  function setSubcontractorActive(subId, active) {
    setSubcontractors(prev => prev.map(s => s.id === subId ? { ...s, status: active ? 'Active' : 'Inactive' } : s));
  }
  // ---- Subcontractor self-registration workflow (§ registration request) —
  // Draft (editable) -> Submitted (locked, awaiting Accounting) -> Approved
  // (locked) -> Edit Requested (locked, awaiting Accounting to release back
  // to Draft). Every transition is logged append-only to registrationHistory. ----
  function regEntry(action, notes) { return makeActivityEntry({ user: currentUserName, action, notes }); }
  function submitSubcontractorRegistration(subId) {
    setSubcontractors(prev => prev.map(s => s.id !== subId ? s : {
      ...s, registrationStatus: 'Submitted', submittedDate: todayISO(), submittedBy: currentUserName,
      registrationHistory: [...s.registrationHistory, regEntry('Submitted registration for approval')],
    }));
  }
  function approveSubcontractorRegistration(subId) {
    setSubcontractors(prev => prev.map(s => s.id !== subId ? s : {
      ...s, registrationStatus: 'Approved', approvedDate: todayISO(), approvedBy: currentUserName,
      registrationHistory: [...s.registrationHistory, regEntry('Approved registration')],
    }));
  }
  function requestSubcontractorChanges(subId, note) {
    setSubcontractors(prev => prev.map(s => s.id !== subId ? s : {
      ...s, registrationStatus: 'Draft',
      registrationHistory: [...s.registrationHistory, regEntry('Sent back for changes', note)],
    }));
  }
  function requestSubcontractorEdit(subId, note) {
    setSubcontractors(prev => prev.map(s => s.id !== subId ? s : {
      ...s, registrationStatus: 'Edit Requested', editRequestedDate: todayISO(), editRequestNote: note || '',
      registrationHistory: [...s.registrationHistory, regEntry('Requested edit access', note)],
    }));
  }
  function releaseSubcontractorForEditing(subId) {
    setSubcontractors(prev => prev.map(s => s.id !== subId ? s : {
      ...s, registrationStatus: 'Draft', editRequestNote: '',
      registrationHistory: [...s.registrationHistory, regEntry('Released for editing')],
    }));
  }
  function linkSubcontractorProject(subId, projectId) {
    setSubcontractors(prev => prev.map(s => s.id === subId && !s.projectIds.includes(projectId) ? { ...s, projectIds: [...s.projectIds, projectId] } : s));
  }
  function unlinkSubcontractorProject(subId, projectId) {
    setSubcontractors(prev => prev.map(s => s.id === subId ? { ...s, projectIds: s.projectIds.filter(id => id !== projectId) } : s));
  }

  // ---- Accounts Payable — Vendor / Freight / Subcontractor invoices (§ AP request) ----
  // Approval Status and Payment Status are always separate fields; approving
  // an invoice never changes its payment status, and recording a payment
  // never changes its approval status.
  function addApInvoice(projectId, data) {
    updateProject(projectId, draft => {
      if (!draft.apInvoices) draft.apInvoices = [];
      const inv = makeApInvoice(data, currentUserName);
      draft.apInvoices.push(inv);
      logAction(draft, `Invoice submitted — ${data.partyType} ${data.vendorName} #${data.invoiceNumber} (${fmtMoney(Number(data.amount) || 0)}).`);
    });
  }
  // Freight invoices can cover several projects at once (§ freight invoice
  // request), billed by line item — each carries its own project/scope AND
  // a charge type (FREIGHT_CHARGE_TYPES), so the dollars land on the right
  // project's actual cost bucket for profitability, not one lump sum. Split
  // into one AP invoice record per project touched (apInvoices lives inside
  // each project — see the same constraint noted on
  // AddSubcontractorInvoiceModal), sharing an invoiceGroupId so they still
  // read as one invoice everywhere they're shown.
  function addFreightInvoice(data) {
    const byProject = {};
    data.lines.forEach(l => { (byProject[l.projectId] = byProject[l.projectId] || []).push(l); });
    const groupId = Object.keys(byProject).length > 1 ? uid('apgroup') : null;
    Object.entries(byProject).forEach(([projectId, projectLines]) => {
      const amount = projectLines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
      const description = projectLines.map(l => l.description).filter(Boolean).join('; ') || 'Freight charges';
      updateProject(projectId, draft => {
        if (!draft.apInvoices) draft.apInvoices = [];
        const inv = makeApInvoice({
          partyType: 'Freight', vendorId: data.partyId, vendorName: data.partyName,
          invoiceNumber: data.invoiceNumber, invoiceDate: data.invoiceDate, dueDate: data.dueDate,
          amount, description, file: data.file, fileUrl: data.fileUrl, notes: data.notes,
          lines: projectLines.map(l => ({ scopeId: l.scopeId || null, chargeType: l.chargeType, description: l.description, amount: Number(l.amount) || 0 })),
          invoiceGroupId: groupId,
        }, currentUserName);
        draft.apInvoices.push(inv);
        projectLines.forEach(l => {
          const lineAmount = Number(l.amount) || 0;
          if (l.scopeId && l.chargeType && lineAmount) {
            const scope = findScope(draft, l.scopeId);
            if (scope) scope.profitability.actual[l.chargeType] = (scope.profitability.actual[l.chargeType] || 0) + lineAmount;
          }
        });
        logAction(draft, `Freight invoice ${data.invoiceNumber} from ${data.partyName} — ${fmtMoney(amount)} allocated across ${projectLines.length} line item(s).`);
      });
    });
  }
  // Job-related expenses that never touch procurement at all (no vendor
  // estimate/PO/PI) — always Miscellaneous partyType, so unplannedCostItems
  // (lib.jsx) picks every one of these up automatically for the
  // Profitability tab's "Unplanned Business Costs" report. A scope+amount
  // also contributes straight to that scope's actual cost (bucket 'other'),
  // same as any other real cost, so margin reflects it even before that
  // report is read.
  // `paidByCardId` records the expense as already paid, on that card — a job
  // cost charged to a company card IS paid, and it has to appear on that card's
  // statement or the card total silently understates what is owed.
  function addMiscInvoice(projectId, data) {
    updateProject(projectId, draft => {
      if (!draft.apInvoices) draft.apInvoices = [];
      const inv = makeApInvoice({ ...data, partyType: 'Miscellaneous' }, currentUserName);
      if (data.paidByCardId) {
        inv.payments = [makeApPayment({ amount: Number(data.amount) || 0, date: data.invoiceDate, creditCardId: data.paidByCardId, reference: data.invoiceNumber }, currentUserName)];
        inv.paymentStatus = 'Paid';
        inv.approvalStatus = 'Approved';
      }
      draft.apInvoices.push(inv);
      const amount = Number(data.amount) || 0;
      if (data.scopeId && amount) {
        const scope = findScope(draft, data.scopeId);
        if (scope) scope.profitability.actual.other = (scope.profitability.actual.other || 0) + amount;
      }
      logAction(draft, `Miscellaneous expense invoice ${data.invoiceNumber} — ${data.expenseCategory || 'Other'} — ${fmtMoney(amount)}.`);
    });
  }
  // Editing one of these has to move the money as well as the text: the old
  // amount comes back off the old scope before the new one goes on, or a
  // corrected figure quietly double-counts. The card payment carries the same
  // amount and date, so it is rewritten in the same pass.
  function updateMiscInvoice(projectId, invId, data) {
    updateProject(projectId, draft => {
      const inv = draft.apInvoices.find(i => i.id === invId);
      if (!inv) return;
      const before = { amount: Number(inv.amount) || 0, scopeId: inv.scopeId, name: inv.vendorName };
      if (before.scopeId && before.amount) {
        const s0 = findScope(draft, before.scopeId);
        if (s0) s0.profitability.actual.other = (s0.profitability.actual.other || 0) - before.amount;
      }
      const amount = Number(data.amount) || 0;
      Object.assign(inv, {
        vendorName: data.vendorName != null ? data.vendorName : inv.vendorName,
        scopeId: data.scopeId != null ? (data.scopeId || null) : inv.scopeId,
        expenseCategory: data.expenseCategory != null ? data.expenseCategory : inv.expenseCategory,
        invoiceDate: data.invoiceDate || inv.invoiceDate,
        dueDate: data.dueDate || inv.dueDate,
        amount,
        description: data.description != null ? data.description : inv.description,
        notes: data.notes != null ? data.notes : inv.notes,
      });
      if (inv.scopeId && amount) {
        const s1 = findScope(draft, inv.scopeId);
        if (s1) s1.profitability.actual.other = (s1.profitability.actual.other || 0) + amount;
      }
      const pay = (inv.payments || []).find(x => x.creditCardId);
      if (pay) { pay.amount = amount; pay.date = inv.invoiceDate; }
      inv.history.push(makeApHistoryEntry(currentUserName, `Edited — ${before.name} ${fmtMoney(before.amount)} → ${inv.vendorName} ${fmtMoney(amount)}.`));
      logAction(draft, `Card charge ${inv.invoiceNumber} edited — ${inv.vendorName}, ${fmtMoney(amount)}.`);
    });
  }
  // Void, never erase — see apInvoiceLive (data.jsx). The money comes back off
  // the scope so the margin is right again the moment it is voided.
  function voidApInvoice(projectId, invId, reason) {
    updateProject(projectId, draft => {
      const inv = draft.apInvoices.find(i => i.id === invId);
      if (!inv || inv.voided) return;
      const amount = Number(inv.amount) || 0;
      if (inv.scopeId && amount) {
        const sc = findScope(draft, inv.scopeId);
        if (sc) sc.profitability.actual.other = (sc.profitability.actual.other || 0) - amount;
      }
      inv.voided = true; inv.voidedBy = currentUserName; inv.voidedDate = todayISO(); inv.voidReason = reason || '';
      inv.history.push(makeApHistoryEntry(currentUserName, `Voided${reason ? ` — ${reason}` : ''}.`));
      logAction(draft, `Invoice ${inv.invoiceNumber} (${inv.vendorName}) voided — ${fmtMoney(amount)} removed from cost.`);
    });
  }
  // Financial Hub Issues (Phase 7) — a payment problem raised against the
  // project itself, distinct from disputing one AP invoice. An open issue
  // with a holdScope cascades a warning to Delivery/Installation Hubs via
  // projectPaymentHold below, until resolved.
  function addFinancialIssue(projectId, data) {
    updateProject(projectId, draft => {
      const issue = makeFinancialIssue(data, currentUserName);
      draft.financialIssues.push(issue);
      logAction(draft, `Financial issue raised${issue.holdScope ? ` — Hold: Payment Not Received (${issue.holdScope})` : ''}.`);
    });
    notify('issue.new', {
      toUserIds: projectWatchers(projectId, data.assigneeIds || []), projectId,
      title: 'Financial issue raised',
      body: `${currentUserName} raised a financial issue: ${data.description || ''}`,
      link: { view: 'project', projectId, tab: 'financials' },
    });
  }
  function setFinancialIssueStatus(projectId, issueId, status) {
    updateProject(projectId, draft => {
      const issue = draft.financialIssues.find(i => i.id === issueId);
      issue.status = status;
      issue.resolvedDate = status === 'Resolved' ? todayISO() : null;
      logAction(draft, `Financial issue marked ${status}.`);
    });
  }
  function setApInvoiceApproval(projectId, invId, status, reason) {
    updateProject(projectId, draft => {
      const inv = draft.apInvoices.find(i => i.id === invId);
      inv.approvalStatus = status;
      if (status === 'Approved') { inv.approvedBy = currentUserName; inv.approvalDate = todayISO(); inv.rejectionReason = null; }
      if (status === 'Rejected' || status === 'Revision Requested') inv.rejectionReason = reason || '';
      inv.history.push(makeApHistoryEntry(currentUserName, `${status}${reason ? ` — ${reason}` : ''}.`));
      logAction(draft, `Invoice #${inv.invoiceNumber} (${inv.vendorName}) marked ${status}.`);
    });
    const invoice = (projects.find(p => p.id === projectId) || { apInvoices: [] }).apInvoices.find(i => i.id === invId);
    if (invoice) notify('approval.decided', {
      toUserIds: projectWatchers(projectId, [invoice.submittedById]), projectId,
      title: `Invoice ${invoice.invoiceNumber} ${status.toLowerCase()}`,
      body: `${currentUserName} marked ${invoice.vendorName}'s invoice ${invoice.invoiceNumber} as ${status}${reason ? ` — ${reason}` : ''}.`,
      link: { view: 'project', projectId, tab: 'financials' },
    });
  }
  function addApPayment(projectId, invId, data) {
    updateProject(projectId, draft => {
      const inv = draft.apInvoices.find(i => i.id === invId);
      inv.payments.push(makeApPayment(data, currentUserName));
      const paid = inv.payments.reduce((s, p) => s + p.amount, 0);
      inv.paymentStatus = paid >= inv.amount ? 'Paid' : paid > 0 ? 'Partially Paid' : 'Unpaid';
      inv.history.push(makeApHistoryEntry(currentUserName, `Payment of ${fmtMoney(Number(data.amount) || 0)} recorded${data.reference ? ` (${data.reference})` : ''}.`));
      logAction(draft, `Payment of ${fmtMoney(Number(data.amount) || 0)} recorded on invoice #${inv.invoiceNumber} (${inv.vendorName}).`);
    });
    // Only once the invoice is actually settled, and only when it names a
    // scope — a part payment has not finished the stage.
    const invNow = ((projects.find(p => p.id === projectId) || {}).apInvoices || []).find(i => i.id === invId);
    if (invNow && invNow.scopeId) {
      const paidNow = (invNow.payments || []).reduce((n, x) => n + (Number(x.amount) || 0), 0) + (Number(data.amount) || 0);
      if (paidNow >= (Number(invNow.amount) || 0)) {
        suggestStage(projectId, { scopeId: invNow.scopeId, stageKey: 'pi_payment', because: `Invoice #${invNow.invoiceNumber} to ${invNow.vendorName} was paid in full` });
      }
    }
  }
  function setApInvoicePaymentStatus(projectId, invId, status) {
    updateProject(projectId, draft => {
      const inv = draft.apInvoices.find(i => i.id === invId);
      inv.paymentStatus = status;
      inv.history.push(makeApHistoryEntry(currentUserName, `Payment status set to ${status}.`));
    });
  }
  function addVendorEstimateRevision(projectId, veId, data) {
    updateProject(projectId, draft => {
      const ve = draft.vendorEstimates.find(v => v.id === veId);
      const revision = (ve.revisions.length ? Math.max(...ve.revisions.map(r => r.revision)) : 0) + 1;
      ve.revisions.push(makeRevisionEntry({ revisionNumber: revision, date: data.date, reasonForRevision: data.reasonForRevision, previousAmount: ve.amount, revisedAmount: data.amount, notes: data.note, file: data.file, fileUrl: data.fileUrl }, currentUserName));
      ve.amount = data.amount;
      logAction(draft, `Added revision ${revision} to estimate ${ve.estimateNumber || ve.vendorName} (${fmtMoney(data.amount)}).`);
    });
  }
  function updateVendorEstimateRevision(projectId, veId, revId, fields) {
    updateProject(projectId, draft => {
      const ve = draft.vendorEstimates.find(v => v.id === veId);
      const r = ve.revisions.find(x => x.id === revId);
      Object.assign(r, fields);
      if (fields.fileUrl) logAction(draft, `Attached quotation file to ${ve.vendorName} Rev ${r.revision}.`);
    });
  }

  // ---- AIA-style billing (Schedule of Values + Payment Applications) ----
  // SOV lines are only ever created/refreshed by applySovSync (from the
  // executed contract) — manual add/remove stays only for lines that
  // predate this feature (sourceType null, grandfathered in normalizeProject).
  function applySovSync(projectId) {
    updateProject(projectId, draft => {
      const { toAdd, toUpdate } = planSovSyncFromContract(draft);
      if (toAdd.length === 0 && toUpdate.length === 0) return;
      let cat = draft.sov.find(c => c.name === 'Contract Scopes');
      if (!cat && toAdd.length) { cat = makeSovCategory('Contract Scopes', []); draft.sov.push(cat); }
      toAdd.forEach(a => {
        const targetCat = a.sourceType === 'changeOrder'
          ? (draft.sov.find(c => c.name === 'Approved Change Orders') || (() => { const c = makeSovCategory('Approved Change Orders', []); draft.sov.push(c); return c; })())
          : cat;
        targetCat.items.push(makeSovItem(a.description, a.scheduledValue, a.sourceType, a.sourceId));
      });
      toUpdate.forEach(u => { u.item.scheduledValue = u.newValue; u.item.description = u.description; });
      logAction(draft, `Synced Schedule of Values from contract: ${toAdd.length} line${toAdd.length === 1 ? '' : 's'} added, ${toUpdate.length} updated.`);
    });
  }
  function removeSovItem(projectId, catId, itemId) {
    updateProject(projectId, draft => {
      const cat = draft.sov.find(c => c.id === catId);
      const item = cat.items.find(i => i.id === itemId);
      const billed = (draft.applications || []).some(a => (a.lines[itemId] || {}).previous || (a.lines[itemId] || {}).current);
      if (billed) { window.alert('This line has already been billed on a payment application and cannot be removed.'); return; }
      cat.items = cat.items.filter(i => i.id !== itemId);
      if (item) logAction(draft, `Removed SOV line "${item.description}".`);
    });
  }
  // Retained only for legacy/manual categories that predate contract-sync —
  // blocked if it still holds any billed line.
  function removeSovCategory(projectId, catId) {
    updateProject(projectId, draft => {
      const cat = draft.sov.find(c => c.id === catId);
      if (!cat) return;
      const billed = cat.items.some(item => (draft.applications || []).some(a => (a.lines[item.id] || {}).previous || (a.lines[item.id] || {}).current));
      if (billed) { window.alert('This category still contains a billed line and cannot be removed.'); return; }
      draft.sov = draft.sov.filter(c => c.id !== catId);
    });
  }
  // Grandfathered manual entry — only reachable for pre-existing categories
  // that already contain manually-typed (sourceType null) lines.
  function addSovItem(projectId, catId, description, scheduledValue) {
    updateProject(projectId, draft => {
      const cat = draft.sov.find(c => c.id === catId);
      cat.items.push(makeSovItem(description, scheduledValue, null, null));
      logAction(draft, `Added SOV line "${description}" (${fmtMoney(scheduledValue)}) to "${cat.name}".`);
    });
  }
  function updateSovItem(projectId, catId, itemId, fields) {
    updateProject(projectId, draft => {
      const cat = draft.sov.find(c => c.id === catId);
      const item = cat.items.find(i => i.id === itemId);
      Object.assign(item, fields);
    });
  }
  function createApplication(projectId, periodTo, preparedBy) {
    setProjects(prev => prev.map(p => p.id === projectId ? createNextApplication(p, periodTo, preparedBy, currentRole, currentUserName) : p));
  }
  const blankAppLine = { previous: 0, current: 0, storedPreviousBalance: 0, storedIncorporated: 0, storedAdded: 0, inputType: 'Amount', formulaText: '', percent: 0, retainageOverride: null };
  function updateApplicationLine(projectId, appId, itemId, fields) {
    updateProject(projectId, draft => {
      const app = draft.applications.find(a => a.id === appId);
      app.lines[itemId] = { ...(app.lines[itemId] || blankAppLine), ...fields };
    });
  }
  function updateApplicationCoLine(projectId, appId, coId, fields) {
    updateProject(projectId, draft => {
      const app = draft.applications.find(a => a.id === appId);
      app.coLines[coId] = { ...(app.coLines[coId] || blankAppLine), ...fields };
    });
  }
  function updateApplicationMeta(projectId, appId, fields) {
    updateProject(projectId, draft => {
      const app = draft.applications.find(a => a.id === appId);
      Object.assign(app, fields);
    });
  }
  function updateAiaApplicationHeader(projectId, appId, fields) {
    setProjects(prev => prev.map(p => p.id === projectId ? setAiaApplicationHeader(p, appId, fields, currentRole, currentUserName) : p));
  }
  function saveAiaHeaderDefaults(projectId, fields) {
    updateProject(projectId, draft => { draft.aiaHeaderDefaults = { ...draft.aiaHeaderDefaults, ...fields }; });
  }
  function setAppRetainageReleased(projectId, appId, released) {
    setProjects(prev => prev.map(p => p.id === projectId ? setApplicationRetainageReleased(p, appId, released, currentRole, currentUserName) : p));
  }
  function setAppPayment(projectId, appId, paymentData) {
    const project = projects.find(p => p.id === projectId);
    const summary = project ? applicationSummary(project, appId) : null;
    const dueSnapshot = summary ? summary.currentPaymentDue : 0;
    setProjects(prev => prev.map(p => p.id === projectId ? setApplicationPayment(p, appId, paymentData, dueSnapshot, currentRole, currentUserName) : p));
  }
  function markAiaApplicationStage(projectId, appId, status) {
    setProjects(prev => prev.map(p => p.id === projectId ? setAiaApplicationStage(p, appId, status, currentRole, currentUserName) : p));
  }
  function certifyApplication(projectId, appId, certifiedAmount) {
    setProjects(prev => prev.map(p => p.id === projectId ? certifyAiaApplication(p, appId, certifiedAmount, currentRole, currentUserName) : p));
  }
  function reopenApplication(projectId, appId, reason) {
    setProjects(prev => prev.map(p => p.id === projectId ? reopenAiaApplication(p, appId, reason, currentRole, currentUserName) : p));
  }
  function voidApplication(projectId, appId, reason) {
    setProjects(prev => prev.map(p => p.id === projectId ? voidAiaApplication(p, appId, reason, currentRole, currentUserName) : p));
  }
  // Creates an AR record from a certified application without duplicating
  // financial data — reuses the existing paymentRequisitions collection,
  // tagged with sourceApplicationId so the two stay linked. Receiving actual
  // payment remains its own separate step (setAppPayment), never implied by
  // certifying or creating this AR record.
  function createArFromApplication(projectId, appId, currentPaymentDue) {
    const app = projects.find(p => p.id === projectId)?.applications.find(a => a.id === appId);
    if (!app) return;
    addPaymentRequisition(projectId, {
      date: todayISO(), amount: currentPaymentDue, type: 'Milestone',
      reference: `AIA Application ${app.number}`, note: `Auto-created from certified Application ${app.number}.`,
      sourceApplicationId: appId,
    });
  }

  // ---- Production (Project -> Scope -> Vendor -> Production Record) ----
  function addProductionRecord(projectId, scopeId, vendorId, vendorName) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      draft.productionRecords.push(makeProductionRecord(scopeId, vendorId, vendorName, currentUserName));
      logAction(draft, `Added production record — ${vendorName} for ${scope ? scope.name : 'scope'}.`);
    });
  }
  function setProductionRecordStatus(projectId, recordId, status) {
    updateProject(projectId, draft => {
      const rec = draft.productionRecords.find(r => r.id === recordId);
      rec.status = status;
      if (status === 'In Production' && !rec.startedDate) rec.startedDate = todayISO();
      if (status === 'Complete' && !rec.completedDate) rec.completedDate = todayISO();
      logAction(draft, `Production record (${rec.vendorName}) status set to ${status}.`);
    });
    // Only when EVERY record on that scope is complete — one vendor finishing
    // does not finish the scope's production stage.
    if (status === 'Complete') {
      const proj = projects.find(p => p.id === projectId) || {};
      const rec = (proj.productionRecords || []).find(r => r.id === recordId);
      if (rec) {
        const siblings = (proj.productionRecords || []).filter(r => r.scopeId === rec.scopeId && r.id !== recordId);
        if (siblings.every(r => r.status === 'Complete')) {
          suggestStage(projectId, { scopeId: rec.scopeId, stageKey: 'production', because: 'Every production record on this scope is now Complete' });
        }
      }
    }
  }
  function updateProductionRecord(projectId, recordId, fields) {
    updateProject(projectId, draft => {
      const rec = draft.productionRecords.find(r => r.id === recordId);
      Object.assign(rec, fields);
      logAction(draft, `Edited production record (${rec.vendorName}).`);
    });
  }
  function addProductionDrawing(projectId, recordId, data) {
    updateProject(projectId, draft => {
      const rec = draft.productionRecords.find(r => r.id === recordId);
      rec.drawings.forEach(d => { d.status = 'Superseded'; });
      const revisionNumber = (rec.drawings.length ? Math.max(...rec.drawings.map(d => d.revisionNumber)) : 0) + 1;
      rec.drawings.push({ id: uid('pd'), revisionNumber, uploadDate: todayISO(), uploadedBy: currentUserName, status: 'Current', ...data });
      logAction(draft, `Uploaded Production Drawing Rev ${revisionNumber} — ${rec.vendorName} (previous revision marked Superseded).`);
    });
  }
  function addProductionPhoto(projectId, recordId, data) {
    updateProject(projectId, draft => {
      const rec = draft.productionRecords.find(r => r.id === recordId);
      const revisionNumber = (rec.photos.length ? Math.max(...rec.photos.map(p => p.revisionNumber)) : 0) + 1;
      rec.photos.push({ id: uid('pp'), revisionNumber, uploadedBy: currentUserName, status: 'Current', ...data });
      logAction(draft, `Added production photo — ${rec.vendorName}.`);
    });
  }
  function addProductionQcReport(projectId, recordId, data) {
    updateProject(projectId, draft => {
      const rec = draft.productionRecords.find(r => r.id === recordId);
      rec.qcReports.forEach(q => { q.status = 'Superseded'; });
      const revisionNumber = (rec.qcReports.length ? Math.max(...rec.qcReports.map(q => q.revisionNumber)) : 0) + 1;
      rec.qcReports.push({ id: uid('qc'), revisionNumber, uploadDate: todayISO(), uploadedBy: currentUserName, status: 'Current', supportingPhotos: [], ...data });
      logAction(draft, `Uploaded QC Report Rev ${revisionNumber} — ${rec.vendorName}: ${data.result}${data.result === 'Fail' ? ' — flagged Not Approved for Shipping.' : '.'}`);
    });
  }

  // ---- Material Specification Library (§2, §4, §14-22) — company-wide, reused across projects ----
  function addMaterial(data) {
    setMaterialLibrary(prev => [...prev, makeMaterial(data, currentUserName)]);
  }
  function updateMaterial(materialId, fields) {
    setMaterialLibrary(prev => prev.map(m => m.id === materialId ? { ...m, ...fields } : m));
  }
  function setMaterialActive(materialId, active) {
    setMaterialLibrary(prev => prev.map(m => m.id === materialId ? { ...m, active } : m));
  }
  function addMaterialDocument(materialId, docType, name, file, fileUrl) {
    setMaterialLibrary(prev => prev.map(m => m.id === materialId
      ? { ...m, documents: [...m.documents, makeMaterialDocument(docType, name, file, fileUrl, currentUserName)] }
      : m));
  }
  function removeMaterialDocument(materialId, docId) {
    setMaterialLibrary(prev => prev.map(m => m.id === materialId ? { ...m, documents: m.documents.filter(d => d.id !== docId) } : m));
  }

  // ---- Appliance & Fixture Specification Library (§ casework appliance/
  // fixture request) — same reuse-by-reference pattern as Materials above ----
  function addApplianceSpec(data) {
    const spec = makeApplianceSpec(data, currentUserName);
    setApplianceLibrary(prev => [...prev, spec]);
    return spec;
  }
  function updateApplianceSpec(specId, fields) {
    setApplianceLibrary(prev => prev.map(s => s.id === specId ? { ...s, ...fields } : s));
  }
  function setApplianceSpecActive(specId, active) {
    setApplianceLibrary(prev => prev.map(s => s.id === specId ? { ...s, active } : s));
  }
  function addApplianceSpecDocument(specId, docType, name, file, fileUrl) {
    setApplianceLibrary(prev => prev.map(s => s.id === specId
      ? { ...s, documents: [...s.documents, makeSpecDocument(docType, name, file, fileUrl, currentUserName)] }
      : s));
  }
  function addFixtureSpec(data) {
    const spec = makeFixtureSpec(data, currentUserName);
    setFixtureLibrary(prev => [...prev, spec]);
    return spec;
  }
  function updateFixtureSpec(specId, fields) {
    setFixtureLibrary(prev => prev.map(s => s.id === specId ? { ...s, ...fields } : s));
  }
  function setFixtureSpecActive(specId, active) {
    setFixtureLibrary(prev => prev.map(s => s.id === specId ? { ...s, active } : s));
  }
  function addFixtureSpecDocument(specId, docType, name, file, fileUrl) {
    setFixtureLibrary(prev => prev.map(s => s.id === specId
      ? { ...s, documents: [...s.documents, makeSpecDocument(docType, name, file, fileUrl, currentUserName)] }
      : s));
  }
  // Per-project instances — link an existing (or newly-created) library spec
  // to a scope, room, and optional shop drawing, without re-uploading a file.
  function addApplianceInstance(projectId, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, data.scopeId);
      draft.applianceInstances.push(makeApplianceInstance(data, currentUserName));
      logAction(draft, `Linked appliance "${data.label}" to ${scope.name}.`);
    });
  }
  function removeApplianceInstance(projectId, instanceId) {
    updateProject(projectId, draft => { draft.applianceInstances = draft.applianceInstances.filter(i => i.id !== instanceId); });
  }
  function addFixtureInstance(projectId, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, data.scopeId);
      draft.fixtureInstances.push(makeFixtureInstance(data, currentUserName));
      logAction(draft, `Linked fixture "${data.label}" to ${scope.name}.`);
    });
  }
  function removeFixtureInstance(projectId, instanceId) {
    updateProject(projectId, draft => { draft.fixtureInstances = draft.fixtureInstances.filter(i => i.id !== instanceId); });
  }

  // ---- Warehouse Hub: Material Allocation & Inventory Control (§ warehouse
  // request) — allocation permission (Sales/Admin/Logistics) is deliberately
  // separate from physical inventory control (Admin/Logistic Manager only for
  // sensitive actions); every allocation change is append-only history. ----
  // Inventory Creation is Admin/Accounting/Logistic Manager only (§1) — a
  // distinct grant from canControlInventory, enforced at the call site (UI)
  // via ctx.canCreateInventory; this function itself trusts the caller the
  // same way every other add* function in this app does.
  function addWarehouseMaterial(data) {
    const material = makeWarehouseMaterial(data, currentUserName);
    material.activityLog = [makeActivityEntry({ user: currentUserName, action: 'Created material record', newValue: `${material.currentStock} ${material.unitOfMeasure}` })];
    setWarehouseMaterials(prev => [...prev, material]);
    return material;
  }
  function updateWarehouseMaterial(materialId, fields) {
    // The category is free text, so the sentence-case rule is applied here as
    // well as in the factory and on load — otherwise a category retyped in the
    // Edit modal would sit in a tab of its own until the next reload.
    if (fields.category !== undefined) fields = { ...fields, category: materialCategoryLabel(fields.category) };
    if (fields.name !== undefined) fields = { ...fields, name: materialNameLabel(fields.name) };
    setWarehouseMaterials(prev => prev.map(m => m.id !== materialId ? m : {
      ...m, ...fields,
      activityLog: [...m.activityLog, makeActivityEntry({ user: currentUserName, action: 'Updated material record', notes: Object.keys(fields).join(', ') })],
    }));
  }
  function addInventoryTransaction(materialId, data) {
    const tx = makeInventoryTransaction({ ...data, materialId, enteredBy: data.enteredBy || currentUserName });
    setInventoryTransactions(prev => [...prev, tx]);
    setWarehouseMaterials(prev => prev.map(m => m.id === materialId ? { ...m, currentStock: m.currentStock + (Number(tx.quantity) || 0) } : m));
    return tx;
  }
  function receiveInventory(materialId, data) {
    return addInventoryTransaction(materialId, { ...data, type: 'Receiving', quantity: Math.abs(Number(data.quantity) || 0), status: data.status || 'Received' });
  }
  function adjustStock(materialId, newQuantity, reason) {
    const material = warehouseMaterials.find(m => m.id === materialId);
    if (!material) return;
    const diff = Number(newQuantity) - material.currentStock;
    addInventoryTransaction(materialId, { type: 'Adjustment', quantity: diff, status: 'Completed', notes: reason || 'Manual stock adjustment' });
  }
  // Confirms what actually arrived from a container and turns THAT (not
  // the original expected quantity) into real Allocations — the manual
  // gate the user asked for, reusing the existing Receiving Report shape
  // (expected/short/over/damaged) rather than a second, parallel one.
  // Never fires automatically off a container's status/actualArrival.
  function receiveContainerMaterials(projectId, containerId, warehouseId, lines) {
    const container = exportContainers.find(c => c.id === containerId);
    if (!container || container.responsibleParty !== 'Logistics') return;
    const scopeIds = containerScopeIdsForProject(container, projectId);
    lines.forEach(line => {
      const containerLine = container.materialLines.find(l => l.id === line.containerLineId);
      if (!containerLine) return;
      const receivedQty = Number(line.receivedQuantity) || 0;
      let warehouseMaterialId = line.warehouseMaterialId;
      let unitOfMeasure = containerLine.unit;
      let unitCost = 0;
      if (!warehouseMaterialId) {
        const created = addWarehouseMaterial({
          name: containerLine.description, category: 'Imported', unitOfMeasure: containerLine.unit,
          warehouseId, relatedPiId: containerLine.piId, currentStock: 0,
        });
        warehouseMaterialId = created.id;
        unitCost = created.unitCost || 0;
      } else {
        const existing = warehouseMaterials.find(m => m.id === warehouseMaterialId);
        if (existing) { unitOfMeasure = existing.unitOfMeasure || unitOfMeasure; unitCost = existing.unitCost || 0; }
      }
      receiveInventory(warehouseMaterialId, {
        quantity: receivedQty, projectId, containerId,
        expectedQuantity: containerLine.quantity, shortQuantity: Number(line.shortQuantity) || 0,
        overQuantity: Number(line.overQuantity) || 0, damagedQuantity: Number(line.damagedQuantity) || 0,
        photos: line.photos || [], notes: line.notes || `Received from container ${container.containerNumber}`,
      });
      // Built directly here rather than via addMaterialAllocation: that
      // function's availability check reads the warehouseMaterials CLOSURE,
      // which still shows this material's pre-receipt stock (0 for a
      // material that's never been received before, and a brand-new material
      // isn't in the closure array at all yet) since the receiveInventory
      // call above only queued its state update — it hasn't re-rendered yet.
      // That was silently rejecting the allocation for exactly what was just
      // received. The quantity here is definitionally available: it's the
      // receipt itself.
      if (receivedQty > 0) {
        const scopeId = scopeIds[0] || null;
        const alloc = makeMaterialAllocation({
          materialId: warehouseMaterialId, projectId, scopeId,
          quantityAllocated: receivedQty, warehouseId, unitOfMeasure,
          notes: `Confirmed received from container ${container.containerNumber}`,
        }, currentUserName);
        alloc.history = [makeAllocationHistoryEntry({ user: currentUserName, material: containerLine.description, previousAllocation: null, newAllocation: receivedQty, newProjectId: projectId, quantity: receivedQty, reason: 'Initial allocation — container receipt' })];
        if (scopeId && unitCost) {
          alloc.costContribution = receivedQty * unitCost;
          adjustScopeActualCost(projectId, scopeId, 'vendorCost', alloc.costContribution);
        }
        setMaterialAllocations(prev => [...prev, alloc]);
      }
    });
    updateProject(projectId, draft => {
      logAction(draft, `Received materials from container ${container.containerNumber} at the warehouse — allocations created for confirmed quantities.`);
    });
  }
  function addMaterialAllocation(data) {
    const material = warehouseMaterials.find(m => m.id === data.materialId);
    if (!material) return { error: 'Material not found.' };
    const qty = Number(data.quantityAllocated) || 0;
    if (qty <= 0) return { error: 'Enter a quantity to allocate.' };
    const available = availableQuantity(material, materialAllocations);
    if (qty > available) return { error: `Only ${available} ${material.unitOfMeasure} available — cannot allocate ${qty}. An authorized inventory adjustment is required first.` };
    const alloc = makeMaterialAllocation(data, currentUserName);
    alloc.history = [makeAllocationHistoryEntry({ user: currentUserName, material: material.name, previousAllocation: null, newAllocation: qty, newProjectId: data.projectId, quantity: qty, reason: 'Initial allocation' })];
    if (data.scopeId && material.unitCost) {
      alloc.costContribution = qty * material.unitCost;
      adjustScopeActualCost(data.projectId, data.scopeId, 'vendorCost', alloc.costContribution);
    }
    setMaterialAllocations(prev => [...prev, alloc]);
    return { ok: true, allocation: alloc };
  }
  function releaseAllocation(allocationId, quantityToRelease, notes) {
    const alloc = materialAllocations.find(a => a.id === allocationId);
    if (!alloc) return;
    const qty = Math.min(Number(quantityToRelease) || 0, alloc.quantityAllocated - alloc.quantityReleased);
    if (qty <= 0) return;
    const newReleased = alloc.quantityReleased + qty;
    const status = newReleased >= alloc.quantityAllocated ? 'Fully Released' : 'Partially Released';
    setMaterialAllocations(prev => prev.map(a => a.id !== allocationId ? a : {
      ...a, quantityReleased: newReleased, status,
      history: [...a.history, makeAllocationHistoryEntry({ user: currentUserName, material: a.materialId, previousAllocation: a.quantityAllocated - a.quantityReleased, newAllocation: a.quantityAllocated - newReleased, originalProjectId: a.projectId, newProjectId: a.projectId, quantity: qty, reason: 'Released to jobsite', notes })],
    }));
    addInventoryTransaction(alloc.materialId, { type: 'Withdrawal', quantity: -qty, status: 'Completed', notes: `Released from allocation ${alloc.id}${notes ? ` — ${notes}` : ''}` });
  }
  function cancelAllocation(allocationId, reason) {
    const alloc = materialAllocations.find(a => a.id === allocationId);
    if (!alloc) return;
    // Only ever reverses the un-released portion's cost — released material
    // has already shipped and is no longer eligible to "return" here.
    const remaining = alloc.quantityAllocated - alloc.quantityReleased;
    if (alloc.scopeId && alloc.costContribution) {
      const material = warehouseMaterials.find(m => m.id === alloc.materialId);
      const unitCost = material ? material.unitCost : (alloc.costContribution / (alloc.quantityAllocated || 1));
      const reversal = remaining * unitCost;
      adjustScopeActualCost(alloc.projectId, alloc.scopeId, 'vendorCost', -reversal);
    }
    setMaterialAllocations(prev => prev.map(a => a.id !== allocationId ? a : {
      ...a, status: 'Cancelled',
      history: [...a.history, makeAllocationHistoryEntry({ user: currentUserName, material: a.materialId, previousAllocation: remaining, newAllocation: 0, originalProjectId: a.projectId, newProjectId: null, quantity: remaining, reason: reason || 'Cancelled' })],
    }));
  }
  function reallocateMaterial(allocationId, newProjectId, newScopeId, data) {
    const old = materialAllocations.find(a => a.id === allocationId);
    if (!old) return;
    const remaining = old.quantityAllocated - old.quantityReleased;
    const material = warehouseMaterials.find(m => m.id === old.materialId);
    const unitCost = material ? material.unitCost : 0;
    if (old.scopeId && unitCost) adjustScopeActualCost(old.projectId, old.scopeId, 'vendorCost', -(remaining * unitCost));
    const newAlloc = makeMaterialAllocation({ ...data, materialId: old.materialId, projectId: newProjectId, scopeId: newScopeId, quantityAllocated: remaining, warehouseId: old.warehouseId, reallocatedFromId: old.id }, currentUserName);
    if (newScopeId && unitCost) { newAlloc.costContribution = remaining * unitCost; adjustScopeActualCost(newProjectId, newScopeId, 'vendorCost', newAlloc.costContribution); }
    newAlloc.history = [makeAllocationHistoryEntry({ user: currentUserName, material: old.materialId, previousAllocation: remaining, newAllocation: remaining, originalProjectId: old.projectId, newProjectId, quantity: remaining, reason: data.reason || 'Reallocated from another project' })];
    setMaterialAllocations(prev => [
      ...prev.map(a => a.id !== allocationId ? a : {
        ...a, status: 'Reallocated',
        history: [...a.history, makeAllocationHistoryEntry({ user: currentUserName, material: a.materialId, previousAllocation: remaining, newAllocation: 0, originalProjectId: a.projectId, newProjectId, quantity: remaining, reason: `Reallocated to new allocation ${newAlloc.id}` })],
      }),
      newAlloc,
    ]);
  }
  // Completed-job leftover materials -> stock (Phase 9). A value is captured
  // at request time (editable, not blindly the material's own unitCost —
  // leftover stock may be worth less) and that's the exact amount reversed
  // off the project's cost once both approvals land; the allocation itself
  // is simply cancelled (freeing the stock back into the general pool),
  // mirroring cancelAllocation but with a caller-supplied reversal value.
  function requestStockConversion(allocationId, data) {
    setMaterialAllocations(prev => prev.map(a => a.id !== allocationId ? a : {
      ...a,
      stockConversionRequest: {
        value: Number(data.value) || 0, notes: data.notes || '', requestedBy: currentUserName, requestedDate: todayISO(),
        adminApproved: false, logisticManagerApproved: false,
      },
    }));
  }
  function decideStockConversion(allocationId, which) {
    const alloc = materialAllocations.find(a => a.id === allocationId);
    if (!alloc || !alloc.stockConversionRequest) return;
    const req = { ...alloc.stockConversionRequest, [which === 'admin' ? 'adminApproved' : 'logisticManagerApproved']: true };
    if (req.adminApproved && req.logisticManagerApproved) {
      const remaining = alloc.quantityAllocated - alloc.quantityReleased;
      if (alloc.scopeId) adjustScopeActualCost(alloc.projectId, alloc.scopeId, 'vendorCost', -req.value);
      setMaterialAllocations(prev => prev.map(a => a.id !== allocationId ? a : {
        ...a, status: 'Converted to Stock', stockConversionRequest: req,
        history: [...a.history, makeAllocationHistoryEntry({ user: currentUserName, material: a.materialId, previousAllocation: remaining, newAllocation: 0, originalProjectId: a.projectId, newProjectId: null, quantity: remaining, reason: `Converted to unassigned stock — value ${fmtMoney(req.value)} (Admin + Logistic Manager approved)` })],
      }));
    } else {
      setMaterialAllocations(prev => prev.map(a => a.id !== allocationId ? a : { ...a, stockConversionRequest: req }));
    }
  }
  // Delivery-confirmation is the single trigger that moves stock from
  // Released to Delivered (§7, explicit instruction) — never called directly
  // from the Warehouse Hub, only from confirmDeliveryReceived below.
  function markAllocationDelivered(allocationId, qty, note) {
    setMaterialAllocations(prev => prev.map(a => {
      if (a.id !== allocationId) return a;
      const releasedNotDelivered = a.quantityReleased - a.quantityDelivered;
      const delta = Math.min(Number(qty) || 0, releasedNotDelivered);
      if (delta <= 0) return a;
      const newDelivered = a.quantityDelivered + delta;
      const status = newDelivered >= a.quantityAllocated ? 'Delivered' : a.status;
      return {
        ...a, quantityDelivered: newDelivered, status,
        history: [...a.history, makeAllocationHistoryEntry({ user: currentUserName, material: a.materialId, previousAllocation: releasedNotDelivered, newAllocation: releasedNotDelivered - delta, originalProjectId: a.projectId, newProjectId: a.projectId, quantity: delta, reason: 'Delivered', notes: note })],
      };
    }));
  }

  // ---- Warehouse Release → Packing List → Delivery hand-off (§5-7) —
  // Release formally moves stock out of the warehouse (reuses the existing
  // releaseAllocation withdrawal logic per line); a Packing List is generated
  // from a Release without re-entry; creating a Delivery from a Packing List
  // pre-fills the existing Project Delivery Tab record; confirming that
  // delivery is the only thing that advances allocations to "Delivered". ----
  async function createWarehouseRelease(projectId, scopeId, lines, notes) {
    const built = lines.map(l => {
      const alloc = materialAllocations.find(a => a.id === l.allocationId);
      return { allocationId: l.allocationId, materialId: alloc ? alloc.materialId : null, quantity: Number(l.quantity) || 0 };
    }).filter(l => l.quantity > 0 && l.materialId);
    if (!built.length) return { error: 'Select at least one line with a quantity to release.' };
    built.forEach(l => releaseAllocation(l.allocationId, l.quantity, notes));
    const release = makeWarehouseRelease({ releaseNumber: await reserveReleaseNumber(), projectId, scopeId, lines: built, notes }, currentUserName);
    setWarehouseReleases(prev => [...prev, release]);
    return { ok: true, release };
  }
  async function generatePackingList(releaseId, selectedLines, extra) {
    const release = warehouseReleases.find(r => r.id === releaseId);
    if (!release) return { error: 'Release not found.' };
    const project = projects.find(p => p.id === release.projectId);
    const scope = project && project.scopes.find(s => s.id === release.scopeId);
    const lines = selectedLines.map(l => {
      const material = warehouseMaterials.find(m => m.id === l.materialId);
      return {
        allocationId: l.allocationId, materialId: l.materialId, itemName: material ? material.name : '—',
        sku: material ? material.itemId || material.referenceNumber : '', description: material ? material.description : '',
        scopeName: scope ? scope.name : '', quantity: Number(l.quantity) || 0, unitOfMeasure: material ? material.unitOfMeasure : 'Units',
        packageInfo: l.packageInfo || '', notes: l.notes || '',
      };
    }).filter(l => l.quantity > 0);
    if (!lines.length) return { error: 'Select at least one line to include on the packing list.' };
    const pl = makePackingList({
      packingListNumber: await reservePackingListNumber(), projectId: release.projectId, scopeId: release.scopeId, releaseId,
      projectName: project ? project.name : '', address: (extra && extra.address) || (project ? project.address : '') || '',
      deliveryContact: (extra && extra.deliveryContact) || '', plannedDeliveryDate: extra && extra.plannedDeliveryDate,
      lines, notes: extra && extra.notes,
    }, currentUserName);
    setPackingLists(prev => [...prev, pl]);
    setWarehouseReleases(prev => prev.map(r => r.id === releaseId ? { ...r, status: 'Packed', packingListId: pl.id } : r));
    return { ok: true, packingList: pl };
  }
  async function createDeliveryFromPackingList(packingListId) {
    const pl = packingLists.find(p => p.id === packingListId);
    if (!pl) return { error: 'Packing list not found.' };
    const description = `Packing List ${pl.packingListNumber} — ${pl.lines.map(l => `${l.quantity} ${l.unitOfMeasure} ${l.itemName}`).join(', ')}`;
    const deliveryNumber = pl.deliveryNumber || await reserveDeliveryNumber();
    let newId = null;
    updateProject(pl.projectId, draft => {
      const del = makeDelivery({
        deliveryNumber, scopeId: pl.scopeId || null, description, date: pl.plannedDeliveryDate || todayISO(), notes: pl.notes || '',
        packingListId: pl.id, warehouseReleaseId: pl.releaseId, jobsiteContact: pl.deliveryContact || '', deliveryAddress: pl.address || draft.address,
        approvalStatus: 'Approved',
        // Copies the release's allocation-linked lines onto the delivery
        // itself (not just the packing list) so the Delivery Driver Hub's
        // proof-of-delivery flow can work from the delivery record alone.
        lines: pl.lines.map(l => ({ id: uid('dline'), allocationId: l.allocationId, materialId: l.materialId, itemName: l.itemName, quantity: l.quantity, unit: l.unitOfMeasure, scopeId: pl.scopeId || null, notes: '', deliveredQuantity: null })),
      }, currentUserName);
      del.approvedBy = currentUserName;
      del.approvedDate = todayISO();
      draft.deliveries.push(del);
      newId = del.id;
      logAction(draft, `Created delivery ${deliveryNumber} from Packing List ${pl.packingListNumber} — ${description}.`);
    });
    setPackingLists(prev => prev.map(p => p.id === packingListId ? { ...p, deliveryId: newId, deliveryNumber } : p));
    return { ok: true, deliveryId: newId };
  }
  function confirmDeliveryReceived(projectId, deliveryId, extra) {
    const project = projects.find(p => p.id === projectId);
    const delivery = project && project.deliveries.find(d => d.id === deliveryId);
    if (!delivery) return;
    updateProject(projectId, draft => {
      const d = draft.deliveries.find(x => x.id === deliveryId);
      d.deliveryStatus = 'Delivered';
      if (extra) Object.assign(d, extra);
      logAction(draft, `Delivery confirmed received — ${d.description}.`);
    });
    if (delivery.packingListId) {
      const pl = packingLists.find(p => p.id === delivery.packingListId);
      if (pl) pl.lines.forEach(l => markAllocationDelivered(l.allocationId, l.quantity, `Delivered via ${pl.packingListNumber}`));
    }
  }
  // ---- Shop Drawing / Submittal & Client Response (§1) — each an append-only revision history ----
  function addSubmittal(projectId, scopeId, docType, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const thread = makeSubmittalThread(scopeId, docType, data.name, data.vendorId, data.vendorName, currentUserName);
      thread.status = data.status;
      thread.revisions = [{ id: uid('sr'), revisionNumber: 0, date: data.date, status: data.status, file: data.file || null, fileUrl: data.fileUrl || null, notes: data.notes || '', responsiblePerson: data.responsiblePerson || currentUserName }];
      (docType === 'Shop Drawing / Submittal' ? scope.submittals : scope.clientResponses).push(thread);
      logAction(draft, `Added ${docType} — "${data.name}" (Rev. 0) for ${scope.name}.`);
    });
  }
  function addSubmittalRevision(projectId, scopeId, threadId, isResponse, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const list = isResponse ? scope.clientResponses : scope.submittals;
      const thread = list.find(t => t.id === threadId);
      thread.revisions.forEach(r => { if (r.status !== 'Superseded') r.status = 'Superseded'; });
      const revisionNumber = thread.revisions.length ? Math.max(...thread.revisions.map(r => r.revisionNumber)) + 1 : 0;
      thread.revisions.push({ id: uid('sr'), revisionNumber, date: data.date, status: data.status, file: data.file || null, fileUrl: data.fileUrl || null, notes: data.notes || '', responsiblePerson: data.responsiblePerson || currentUserName });
      thread.status = data.status;
      logAction(draft, `Added revision Rev. ${revisionNumber} to "${thread.name}" — status: ${data.status}.`);
    });
    // A client response is the client answering; it does not finish OUR drawing
    // stage, so only a submittal revision suggests anything.
    if (!isResponse) suggestStage(projectId, { scopeId, stageKey: 'shop_drawings', because: `A new revision was added to "${(((projects.find(p => p.id === projectId) || {}).scopes || []).find(sc => sc.id === scopeId) || {}).name || 'the scope'}" submittals` });
  }
  // A client answering a submittal from their portal. It creates the SAME
  // clientResponses thread a coordinator would log by hand, and moves the
  // submittal's own status — so the Shop Drawing Hub shows one history, not a
  // portal history alongside an internal one. The client is only ever allowed
  // to set an outcome the workflow already has (SUBMITTAL_STATUSES).
  function clientRespondToSubmittal(projectId, scopeId, threadId, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const submittal = (scope.submittals || []).find(t => t.id === threadId);
      if (!submittal) return;
      const latest = submittal.revisions[submittal.revisions.length - 1];
      const thread = makeSubmittalThread(scopeId, 'Client Response / Submittal Response',
        `${submittal.name} — client response`, null, '', currentUserName);
      thread.respondingToSubmittalId = threadId;
      thread.respondingToRevisionNumber = latest ? latest.revisionNumber : 0;
      thread.status = data.status;
      thread.viaClientPortal = true;
      thread.revisions = [{
        id: uid('sr'), revisionNumber: 0, date: todayISO(), status: data.status,
        file: data.file || null, fileUrl: data.fileUrl || null,
        notes: data.notes || '', responsiblePerson: currentUserName,
      }];
      if (!scope.clientResponses) scope.clientResponses = [];
      scope.clientResponses.push(thread);
      submittal.status = data.status;
      submittal.clientRespondedDate = todayISO();
      submittal.clientRespondedBy = currentUserName;
      logAction(draft, `Client responded through the portal to "${submittal.name}" Rev. ${latest ? latest.revisionNumber : 0} — ${data.status}.`);
      draft.__notify = [...(draft.__notify || []), {
        event: 'submittal.clientResponse',
        toUserIds: projectWatchers(projectId, []),
        title: `Client responded: ${data.status}`,
        body: `${currentUserName} responded to "${submittal.name}" on ${scope.name} — ${data.status}${data.notes ? ` — ${data.notes}` : ''}`,
        link: { view: 'project', projectId, tab: 'documents' },
      }];
    });
    drainDraftNotices(projectId);
  }
  // ---- Quality control -------------------------------------------------
  // Areas are set up first, then inspections are booked against them. Both are
  // scheduled and managed by the Production Director and the associates
  // (module 'qc'); everyone else sees the same records read-only.
  function addQcArea(projectId, data) {
    updateProject(projectId, draft => {
      draft.qcAreas = draft.qcAreas || [];
      draft.qcAreas.push(makeQcArea(data, currentUserName));
      logAction(draft, `Added QC area "${data.name}".`);
    });
  }
  function updateQcArea(projectId, areaId, fields) {
    updateProject(projectId, draft => {
      const a = (draft.qcAreas || []).find(x => x.id === areaId);
      if (!a) return;
      Object.assign(a, fields);
      logAction(draft, `Edited QC area "${a.name}".`);
    });
  }
  function removeQcArea(projectId, areaId) {
    updateProject(projectId, draft => {
      const a = (draft.qcAreas || []).find(x => x.id === areaId);
      if (!a) return;
      a.active = false;
      logAction(draft, `Retired QC area "${a.name}".`);
    });
  }
  function addQcInspection(projectId, data) {
    updateProject(projectId, draft => {
      draft.qcInspections = draft.qcInspections || [];
      const insp = makeQcInspection(data, currentUserName);
      draft.qcInspections.push(insp);
      const area = (draft.qcAreas || []).find(a => a.id === insp.areaId);
      logAction(draft, `Scheduled QC inspection${area ? ` for ${area.name}` : ''} on ${fmtDate(insp.scheduledDate)} — ${insp.inspectorType}.`);
      if (insp.inspectorId) draft.__notify = [...(draft.__notify || []), {
        event: 'assignment.new', toUserIds: [insp.inspectorId],
        title: 'QC inspection booked in your name',
        body: `${insp.title || 'Inspection'}${area ? ` — ${area.name}` : ''} on ${fmtDate(insp.scheduledDate)}`,
        link: { view: 'project', projectId, tab: 'qc' },
      }];
    });
    drainDraftNotices(projectId);
  }
  function updateQcInspection(projectId, inspId, fields) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (!i) return;
      Object.assign(i, fields);
      logAction(draft, `Edited QC inspection${i.title ? ` "${i.title}"` : ''}.`);
    });
  }
  function removeQcInspection(projectId, inspId) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (!i) return;
      i.status = 'Cancelled';
      logAction(draft, `Cancelled QC inspection${i.title ? ` "${i.title}"` : ''}.`);
    });
  }
  function setQcCheckResult(projectId, inspId, itemId, fields) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      const c = i && i.checklist.find(x => x.id === itemId);
      if (!c) return;
      Object.assign(c, fields);
      if (i.status === 'Scheduled') { i.status = 'In Progress'; i.startedDate = todayISO(); }
    });
  }
  function addQcCheckItem(projectId, inspId, text) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (i) i.checklist.push(makeQcCheckItem(text));
    });
  }
  function removeQcCheckItem(projectId, inspId, itemId) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (i) i.checklist = i.checklist.filter(c => c.id !== itemId);
    });
  }
  function addQcPhoto(projectId, inspId, name, url) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (i) i.photos.push({ id: uid('qcphoto'), name, url, by: currentUserName, date: todayISO() });
    });
  }
  function removeQcPhoto(projectId, inspId, photoId) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (i) i.photos = i.photos.filter(x => x.id !== photoId);
    });
  }
  // Recording the result is the one action an inspector takes, and the one the
  // team is notified about. A result that is not a clean pass carries its
  // reason, because a bare "failed" gives the shop nothing to act on.
  function submitQcResult(projectId, inspId, result, notes) {
    updateProject(projectId, draft => {
      const i = (draft.qcInspections || []).find(x => x.id === inspId);
      if (!i) return;
      i.result = result; i.status = result; i.notes = notes || i.notes;
      i.completedDate = todayISO();
      if (!i.startedDate) i.startedDate = todayISO();
      i.activityLog.push({ id: uid('qcl'), date: todayISO(), by: currentUserName, action: `Recorded result: ${result}${notes ? ` — ${notes}` : ''}` });
      const area = (draft.qcAreas || []).find(a => a.id === i.areaId);
      logAction(draft, `QC inspection${area ? ` — ${area.name}` : ''} recorded as ${result} by ${currentUserName}.`);
      draft.__notify = [...(draft.__notify || []), {
        event: result === 'Failed' ? 'issue.new' : 'approval.decided',
        toUserIds: projectWatchers(projectId, []),
        title: `QC ${result}${area ? ` — ${area.name}` : ''}`,
        body: `${currentUserName} recorded ${result}${notes ? ` — ${notes}` : ''}`,
        link: { view: 'project', projectId, tab: 'qc' },
      }];
    });
    drainDraftNotices(projectId);
  }

  function addClientResponse(projectId, scopeId, respondingToSubmittalId, respondingToRevisionNumber, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      const thread = makeSubmittalThread(scopeId, 'Client Response / Submittal Response', data.name, data.vendorId, data.vendorName, currentUserName);
      thread.respondingToSubmittalId = respondingToSubmittalId;
      thread.respondingToRevisionNumber = respondingToRevisionNumber;
      thread.status = data.status;
      thread.revisions = [{ id: uid('sr'), revisionNumber: 0, date: data.date, status: data.status, file: data.file || null, fileUrl: data.fileUrl || null, notes: data.notes || '', responsiblePerson: data.responsiblePerson || currentUserName }];
      scope.clientResponses.push(thread);
      const submittal = scope.submittals.find(s => s.id === respondingToSubmittalId);
      logAction(draft, `Logged client response — "${data.name}" responding to ${submittal ? submittal.name : 'submittal'} Rev. ${respondingToRevisionNumber}.`);
    });
  }

  // ---- Window & Exterior Door System schedule (§ Window Schedule template) ----
  // The three approval trackers are thin pointers into a real scope.submittals
  // thread, not their own attachments/comments/revision history — this
  // creates that thread the normal way and links windowSchedule.approvals[
  // approvalKey].submittalThreadId to it. Every revision after this one goes
  // through the existing, unmodified addSubmittalRevision — no new revision/
  // attachment/comment logic exists for this feature at all.
  const WINDOW_APPROVAL_DOC_TYPES = { profileSystem: 'Window Profile/System Approval', glass: 'Window Glass Approval', fabricationDrawing: 'Window Fabrication Drawing Approval' };
  function addWindowApprovalThread(projectId, scopeId, approvalKey, data) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope.windowSchedule) return;
      const docType = WINDOW_APPROVAL_DOC_TYPES[approvalKey];
      const thread = makeSubmittalThread(scopeId, docType, data.name || docType, data.vendorId, data.vendorName, currentUserName, data.requiredByDate);
      thread.status = data.status || 'Draft';
      thread.revisions = [{ id: uid('sr'), revisionNumber: 0, date: data.date || todayISO(), status: thread.status, file: data.file || null, fileUrl: data.fileUrl || null, notes: data.notes || '', responsiblePerson: data.responsiblePerson || currentUserName }];
      scope.submittals.push(thread);
      scope.windowSchedule.approvals[approvalKey].submittalThreadId = thread.id;
      scope.windowSchedule.approvals[approvalKey].requiredByDate = data.requiredByDate || null;
      logAction(draft, `Linked ${docType} thread "${thread.name}" on ${scope.name}.`);
    });
  }

  // Revision-impact workflow (§ Change-Order/Revision-impact workflow) —
  // classifies a Window Schedule change as No Impact / Minor / Major /
  // Change Order Required. "Change Order Required" creates a REAL entry in
  // project.changeOrders (same array/shape as every other CO in the app,
  // just tagged with scopeId/windowRevisionId) rather than a shadow copy —
  // it therefore shows up in the existing Change Orders UI unmodified.
  // Revision numbering follows the same "count from the meaningful
  // checkpoint" convention as the selections-lock fix above, for
  // consistency between the two revision-tracking systems.
  function logWindowRevision(projectId, scopeId, { description, impact, materialAffected, procurementNeeded, coData }) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope.windowSchedule) return;
      const ws = scope.windowSchedule;
      const revisionNumber = (ws.revisions.length ? Math.max(...ws.revisions.map(r => r.revisionNumber)) : 0) + 1;
      let changeOrderId = null;
      if (impact === 'Change Order Required' && coData) {
        const amount = coData.type === 'Back Charge' ? -Math.abs(coData.amount) : Math.abs(coData.amount);
        const co = { id: uid('co'), number: null, type: coData.type || 'Change Order', amount, date: coData.date || todayISO(), file: coData.file || null, fileUrl: coData.fileUrl || null, description: coData.description || description, status: 'Pending', scopeId, windowRevisionId: null };
        draft.changeOrders.push(co);
        changeOrderId = co.id;
        logAction(draft, `Added ${co.type} — ${fmtMoney(amount)} for ${scope.name} (Pending approval; number assigned on approval).`);
      }
      const revision = { id: uid('winrev'), date: todayISO(), description, impact, materialAffected: !!materialAffected, procurementNeeded: !!procurementNeeded, changeOrderId, revisionNumber, loggedBy: currentUserName };
      ws.revisions.push(revision);
      if (changeOrderId) draft.changeOrders.find(c => c.id === changeOrderId).windowRevisionId = revision.id;
      logAction(draft, `Logged Window Schedule Revision ${revisionNumber} on ${scope.name} — impact: ${impact}.`);
    });
  }

  // ---- Window Schedule integration wiring (§ Phase 6 — link, don't duplicate) ----
  // A node never OWNS cost/vendor/status data — it holds ids into the real
  // Procurement/Production/Export/Delivery records, which every existing
  // module's own UI still fully owns. `field` is one of the linkedRecords
  // keys (data.jsx buildWindowSchedule): the four *Ids arrays toggle
  // membership (link/unlink), the two singular ids (submittalThreadId,
  // productionRecordId) just get set/cleared.
  const WINDOW_LINK_ARRAY_FIELDS = ['vendorEstimateIds', 'purchaseOrderIds', 'exportContainerIds', 'deliveryIds'];
  function linkWindowNodeRecord(projectId, scopeId, nodeId, field, recordId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope.windowSchedule) return;
      const node = scope.windowSchedule.nodes.find(n => n.id === nodeId);
      if (!node) return;
      if (WINDOW_LINK_ARRAY_FIELDS.includes(field)) {
        const arr = node.linkedRecords[field];
        const idx = arr.indexOf(recordId);
        if (idx === -1) arr.push(recordId); else arr.splice(idx, 1);
      } else {
        node.linkedRecords[field] = node.linkedRecords[field] === recordId ? null : recordId;
      }
      logAction(draft, `Updated linked records on Window Schedule node "${node.name}" (${scope.name}).`);
    });
  }

  // Unlimited phased/building delivery tracking (§ Window Schedule template,
  // requirement #7) — each phase is both a windowSchedule.deliveryPhases
  // entry (name/status/dates) AND a real DAG node (dependsOn the Shipping
  // node) so it gets its own forecast date from the same engine, not a
  // freehand date.
  function addWindowDeliveryPhase(projectId, scopeId, name, plannedDate) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope.windowSchedule) return;
      const ws = scope.windowSchedule;
      const shippingNode = ws.nodes.find(n => n.key === 'shipping');
      const node = {
        id: uid('winnode'), key: `delivery_phase_${uid('')}`, name: `Delivery — ${name}`,
        dependsOn: shippingNode ? [shippingNode.id] : [], duration: 0,
        baselineStart: null, baselineEnd: null, forecastStart: null, forecastEnd: null,
        actualStart: null, actualEnd: null, status: 'Not Started',
        delayDays: 0, delayReason: null, delayNote: null,
        linkedRecords: { vendorEstimateIds: [], purchaseOrderIds: [], submittalThreadId: null, productionRecordId: null, exportContainerIds: [], deliveryIds: [] },
      };
      ws.nodes.push(node);
      ws.deliveryPhases.push({ id: uid('winphase'), name, nodeId: node.id, plannedDate: plannedDate || null, forecastDate: null, actualDate: null, status: 'Not Started', exportContainerIds: [], deliveryIds: [] });
      computeWindowSchedule(ws);
      logAction(draft, `Added delivery phase "${name}" to Window Schedule on ${scope.name}.`);
    });
  }
  function linkWindowPhaseRecord(projectId, scopeId, phaseId, field, recordId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope.windowSchedule) return;
      const phase = scope.windowSchedule.deliveryPhases.find(p => p.id === phaseId);
      if (!phase) return;
      const arr = phase[field];
      const idx = arr.indexOf(recordId);
      if (idx === -1) arr.push(recordId); else arr.splice(idx, 1);
      logAction(draft, `Updated linked records on delivery phase "${phase.name}" (${scope.name}).`);
    });
  }

  // ---- Selections: optional link to a Material Library record (§4, §21) ----
  function setScopeMaterialLink(projectId, scopeId, categoryId, materialId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      scope.materialLinks[categoryId] = materialId || null;
    });
  }

  // ---- Projected Profitability (§6-8) — Accounting/Admin only ----
  function updateScopeProfitability(projectId, scopeId, fields) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      Object.assign(scope.profitability, fields);
    });
  }
  function updateScopeCostField(projectId, scopeId, which, key, value) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      scope.profitability[which][key] = value;
    });
  }
  // Warehouse allocations flow into the project's *actual* cost automatically
  // (§4, explicit instruction) via a delta, not an absolute set — so a manual
  // entry the user typed into the same field is never clobbered, and the
  // exact amount can be reversed later (cancel/reallocate) without
  // recomputing from scratch.
  function adjustScopeActualCost(projectId, scopeId, key, delta) {
    if (!projectId || !scopeId || !delta) return;
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      if (!scope) return;
      scope.profitability.actual[key] = (scope.profitability.actual[key] || 0) + delta;
    });
  }
  function lockScopeBaseline(projectId, scopeId) {
    updateProject(projectId, draft => {
      const scope = findScope(draft, scopeId);
      scope.profitability.baseline = {
        salesValue: scope.profitability.salesValue,
        costs: { ...scope.profitability.costs },
        targetMarginPct: scope.profitability.targetMarginPct,
        lockedDate: todayISO(), lockedBy: currentUserName,
      };
      logAction(draft, `Locked Original Profitability Baseline for ${scope.name}.`);
    });
  }

  // ---- User management (Admin/Accounting) ----
  function addUser(data) {
    const u = normalizeTeamMember({ id: uid('person'), roles: data.roles || [], active: true, ...data });
    setTeamDirectory(prev => [...prev, u]);
  }
  function updateUser(userId, fields) {
    setTeamDirectory(prev => prev.map(p => p.id === userId ? { ...p, ...fields } : p));
  }
  function setUserActive(userId, active) {
    setTeamDirectory(prev => prev.map(p => p.id === userId ? { ...p, active } : p));
  }

  // ---- Scope library admin ops ----
  function updateLib(mutator) {
    setScopeLibrary(prev => { const draft = cloneDeep(prev); mutator(draft); return draft; });
  }
  const lib = {
    addFamily: (name) => updateLib(d => d.push(makeFamily(name, []))),
    renameFamily: (id, name) => updateLib(d => { findFamilyById(d, id).name = name; }),
    toggleFamily: (id) => updateLib(d => { const f = findFamilyById(d, id); f.active = !f.active; }),
    moveFamily: (id, dir) => updateLib(d => moveItem(d, id, dir)),
    addCategory: (famId, name) => updateLib(d => { findFamilyById(d, famId).categories.push(makeCategory(name, [])); }),
    renameCategory: (catId, name) => updateLib(d => { findCategoryPath(d, catId).category.name = name; }),
    toggleCategory: (catId) => updateLib(d => { const c = findCategoryPath(d, catId).category; c.active = !c.active; }),
    moveCategory: (famId, catId, dir) => updateLib(d => moveItem(findFamilyById(d, famId).categories, catId, dir)),
    addOption: (catId, name, imageUrl) => updateLib(d => { findCategoryPath(d, catId).category.options.push(makeOption(name, imageUrl)); }),
    // Bulk-add supplier finishes into a selection category — the bridge from a
    // supplier's 7,000-item range to the handful LEON actually offers on a
    // scope. Skips anything already there by name so re-running is safe.
    addOptionsBulk: (catId, items) => updateLib(d => {
      const cat = findCategoryPath(d, catId).category;
      if (!cat) return;
      const existing = new Set(cat.options.map(o => (o.name || '').trim().toLowerCase()));
      items.forEach(it => {
        const name = (it.name || '').trim();
        if (!name || existing.has(name.toLowerCase())) return;
        existing.add(name.toLowerCase());
        cat.options.push(makeOption(name, it.img || null));
      });
    }),
    // Create a category and fill it in one commit — the modal can't read back a
    // newly-created category's id from its own stale ctx closure.
    addCategoryWithOptions: (famId, name, items) => updateLib(d => {
      const cat = makeCategory(name, []);
      const existing = new Set();
      (items || []).forEach(it => {
        const n = (it.name || '').trim();
        if (!n || existing.has(n.toLowerCase())) return;
        existing.add(n.toLowerCase());
        cat.options.push(makeOption(n, it.img || null));
      });
      findFamilyById(d, famId).categories.push(cat);
    }),
    renameOption: (optId, name) => updateLib(d => { findOptionPath(d, optId).option.name = name; }),
    toggleOption: (optId) => updateLib(d => { const o = findOptionPath(d, optId).option; o.active = !o.active; }),
    moveOption: (catId, optId, dir) => updateLib(d => moveItem(findCategoryPath(d, catId).category.options, optId, dir)),
    setOptionImage: (optId, imageUrl) => updateLib(d => { findOptionPath(d, optId).option.imageUrl = imageUrl; }),
    toggleFamilyWindowFlag: (id) => updateLib(d => { const f = findFamilyById(d, id); f.isWindowSystem = !f.isWindowSystem; }),
    toggleFamilyCountertopFlag: (id) => updateLib(d => { const f = findFamilyById(d, id); f.isCountertopSystem = !f.isCountertopSystem; }),
  };

  function updateCompanyProfile(fields) { setCompanyProfile(prev => ({ ...prev, ...fields })); }
  // Other locations the company works out of. Soft-deleted like everything
  // else, so an address that appears on an old document never dangles.
  function addCompanyOffice(data) {
    setCompanyProfile(prev => ({ ...prev, offices: [...(prev.offices || []), makeCompanyOffice(data)] }));
  }
  function updateCompanyOffice(id, fields) {
    setCompanyProfile(prev => ({ ...prev, offices: (prev.offices || []).map(o => o.id === id ? { ...o, ...fields } : o) }));
  }
  function removeCompanyOffice(id) {
    setCompanyProfile(prev => ({ ...prev, offices: (prev.offices || []).map(o => o.id === id ? { ...o, active: false } : o) }));
  }

  const visibleProjects = useMemo(
    () => visibleProjectsFor(projects, effectiveUserId, currentRole),
    [projects, effectiveUserId, currentRole]);

  // ---- LEON Sign ---------------------------------------------------------
  // Every mutator writes an EVENT. An envelope's whole value is that it can
  // answer "who did what, when" afterwards, and a change with no event is a
  // change that cannot be answered for. The clock is the browser's own, which
  // the screen states rather than hides.
  function signEvent(kind, detail, extra) {
    return makeSignEvent(Object.assign({
      kind, detail, by: currentUserName, byId: effectiveUserId,
    }, extra || {}));
  }
  function updateSignEnvelope(envId, mutate, eventOrNull) {
    setSignEnvelopes(prev => prev.map(e => {
      if (e.id !== envId) return e;
      const draft = cloneDeep(e);
      mutate(draft);
      if (eventOrNull) draft.events = [eventOrNull].concat(draft.events || []);
      return draft;
    }));
  }
  function addSignEnvelope(data) {
    const n = signCounter;
    setSignCounter(n + 1);
    const env = makeSignEnvelope(Object.assign({
      number: `SIGN-${String(n).padStart(4, '0')}`,
      createdBy: currentUserName,
    }, data || {}));
    env.events = [signEvent('created', `Envelope created${env.subject ? ` — ${env.subject}` : ''}.`)];
    setSignEnvelopes(prev => [env].concat(prev));
    return env.id;
  }
  function removeSignEnvelope(envId) {
    setSignEnvelopes(prev => prev.filter(e => e.id !== envId));
  }
  // Sending is the act that freezes the document. The hash is taken HERE, so
  // "the bytes have not changed since it went out" is a question that can be
  // answered later rather than assumed.
  async function sendSignEnvelope(envId) {
    const env = (signEnvelopes || []).find(e => e.id === envId);
    if (!env) return;
    const blockers = signSendBlockers(env);
    if (blockers.length) { alert('This envelope cannot go yet:\n\n\u2022 ' + blockers.join('\n\u2022 ')); return; }
    const hashes = {};
    for (const d of env.documents || []) { hashes[d.id] = await signHashDataUrl(d.url); }
    const now = new Date().toISOString();
    updateSignEnvelope(envId, draft => {
      draft.sentDate = now;
      draft.status = 'Sent';
      (draft.documents || []).forEach(d => { if (hashes[d.id]) d.sha256 = hashes[d.id]; });
      const first = signCurrentOrder(draft);
      (draft.recipients || []).forEach(r => {
        if ((qnum(r.order) || 1) === first) { r.status = 'Sent'; r.sentDate = now; }
      });
      draft.events = [signEvent('sent', `Sent to ${(draft.recipients || [])
        .filter(r => (qnum(r.order) || 1) === first).map(r => r.name).join(', ')}.`,
        { sha256: hashes[(draft.documents || [])[0] && (draft.documents || [])[0].id] || null })]
        .concat(draft.events || []);
    });
    // An envelope reaches a colleague's inbox and queues an email, exactly as a
    // share does — the Hub cannot send mail and says so in the same words.
    (env.recipients || []).forEach(r => {
      if (r.userId) notify('sign.requested', { toUserIds: [r.userId],
        title: `Signature requested — ${env.subject || env.number}`,
        body: `${currentUserName} has asked you to sign ${env.subject || env.number}.`,
        projectId: env.projectId || null });
    });
  }
  function recordSignSignature(envId, recipientId, mark) {
    const now = new Date().toISOString();
    updateSignEnvelope(envId, draft => {
      const r = (draft.recipients || []).find(x => x.id === recipientId);
      if (!r) return;
      r.status = 'Signed'; r.signedDate = now;
      if (mark && mark.image) r.signatureImage = mark.image;
      if (mark && mark.typed) r.signatureTyped = mark.typed;
      // Auto fields answer themselves — a date signed nobody typed is more
      // reliable than one they did.
      (draft.fields || []).filter(f => f.recipientId === recipientId).forEach(f => {
        if (f.type === 'dateSigned' && !f.value) f.value = now.slice(0, 10);
        if (f.type === 'name' && !f.value) f.value = r.name || '';
      });
      draft.events = [signEvent('signed', `${r.name} signed.`, { recipientId })].concat(draft.events || []);
      // Whoever is next is asked now, which is what makes routing order mean
      // something rather than being a number on a list.
      const next = signCurrentOrder(draft);
      if (next !== null) {
        (draft.recipients || []).forEach(x => {
          if ((qnum(x.order) || 1) === next && x.status === 'Pending') { x.status = 'Sent'; x.sentDate = now; }
        });
      } else {
        draft.completedDate = now;
        draft.events = [signEvent('completed', 'Every signer has signed.')].concat(draft.events);
      }
    }, null);
  }
  function declineSignEnvelope(envId, recipientId, reason) {
    updateSignEnvelope(envId, draft => {
      const r = (draft.recipients || []).find(x => x.id === recipientId);
      if (r) { r.status = 'Declined'; r.declinedReason = reason || ''; }
    }, signEvent('declined', `Declined${reason ? ` — ${reason}` : ''}.`, { recipientId }));
  }
  function voidSignEnvelope(envId, reason) {
    updateSignEnvelope(envId, draft => { draft.status = 'Voided'; draft.voidedReason = reason || ''; },
      signEvent('voided', `Voided${reason ? ` — ${reason}` : ''}.`));
  }
  function signMarkViewed(envId, recipientId) {
    updateSignEnvelope(envId, draft => {
      const r = (draft.recipients || []).find(x => x.id === recipientId);
      if (r && r.status === 'Sent') { r.status = 'Viewed'; r.viewedDate = new Date().toISOString(); }
    }, null);
  }


  const ctx = {
    // `projects` is the set this person may SEE. Filtering here rather than at
    // each list means every dashboard, report, calendar, search and pipeline
    // inherits it — there is no screen left to remember. `allProjects` is the
    // unfiltered set, for the few places that must count or number across the
    // whole company rather than show anything.
    accounts, projects: visibleProjects, allProjects: projects, scopeLibrary, teamDirectory, documentLibrary, companyProfile, updateCompanyProfile,
    addCompanyOffice, updateCompanyOffice, removeCompanyOffice, vendors, freightForwarders, materialLibrary, applianceLibrary, fixtureLibrary,
    scopeDocuments, addScopeDocument, updateScopeDocument, removeScopeDocument,
    windowLeadTimeLibrary, addWindowLeadTimeEntry, updateWindowLeadTimeEntry, setWindowLeadTimeEntryActive,
    interiorLeadTimeLibrary, addInteriorLeadTimeEntry, updateInteriorLeadTimeEntry, setInteriorLeadTimeStageDays, setInteriorLeadTimeEntryActive, resetInteriorLeadTimeEntry,
    setScopeTemplateStages, resetScopeTemplate,
    complexityLevels, addComplexityLevel, updateComplexityLevel, removeComplexityLevel,
    holidays, addHoliday, updateHoliday, removeHoliday,
    clientRespondToSubmittal,
    stageSuggestion, acceptStageSuggestion, dismissStageSuggestion: () => setStageSuggestion(null),
    setProjectPrivacy,
    addQcArea, updateQcArea, removeQcArea, addQcInspection, updateQcInspection, removeQcInspection,
    setQcCheckResult, addQcCheckItem, removeQcCheckItem, addQcPhoto, removeQcPhoto, submitQcResult,
    setProjectContact, setSupplierFinish, setScopeMainAreaName, renameSelectionArea, setAreaSupplierFinish, assignTeam,
    navMemo, rememberNav,
    supplierFinishOverrides, updateSupplierFinish, setSupplierFinishHidden, resetSupplierFinish,
    supplierVendorLinks, linkSupplierVendor,
    importedFinishes: importedFinishes_, addImportedFinishes, removeImportedFinishBatch,
    rolePermissions, permissionLog, setRoleModulePermission, setRoleCapability, resetRolePermissions, resetAllRolePermissions,
    canSeeAccountingHub: canSeeAcctHub,
    canManageCollection: canManageCollection(currentRole),
    canApproveStockConversion: canApproveStockConversion(currentRole),
    canEditDoorLibrary: canEditDoorLibrary(currentRole),
    canEditCaseworkLibrary: canEditCaseworkLibrary(currentRole),
    warehousePackages, setWarehousePackages,
    myDepartments, canChooseDepartment, activeDepartment, departmentChoice, setDepartmentChoice,
    deptScopes, deptProjects, deptRecords,
    warehouses, warehouseMaterials, inventoryTransactions, materialAllocations, warehouseReleases, packingLists,
    canAllocateMaterial: canAllocateMaterial(currentUser),
    canControlInventory: canControlInventory(currentRole),
    canAssistWarehouse: canAssistWarehouse(currentRole),
    canCreateInventory: canCreateInventory(currentRole),
    currentRole, currentUserId: currentUser ? currentUser.id : null, setCurrentUserId, currentUserName, currentUser,
    realCurrentUser, canViewAsOthers, isViewingAs, viewAsUserId, setViewAsUserId,
    canApproveInvoices: canApproveInvoices(currentRole),
    canChangePipelineStatus: ['Admin', 'Accounting'].includes(currentRole),
    authedUserId, logout,
    // Shared reference data (phase 1) — status, and the one-time publish.
    syncState, publishReferenceData,
    canSeeFin, canEdit, canView,
    addAccount, updateAccount, addClientPortalLogin, addProject, loadDemoScenario, removeDemoScenario, setProjectImage, removeProjectImage,
    reportDelay, startStage, completeStage, undoStageChange, addRevisionStage, assignStageUser, canAssignStage: canAssignStage(currentRole), canEditProjectInfo: canEditProjectInfo(currentRole), addScope, setSelection, setScopeSelectionsLocked, addSelectionArea, removeSelectionArea, setAreaSelection,
    reportWindowScheduleDelay,
    reportChronologyDelay, startChronologyStage, completeChronologyStage, assignChronologyUser, undoChronologyChange,
    personalItems, addPersonalItem, updatePersonalItem, setPersonalItemStatus, removePersonalItem,
    addDocument, addProjectDocument, updateScopeDocument, removeScopeDocument,
    addScopeDocumentSection, renameScopeDocumentSection, removeScopeDocumentSection,
    requestAiTakeoff, updateAiTakeoff, removeAiTakeoff,
    addDrawingSet, voidDrawingSet, updateDrawingSet, addTakeOff, removeTakeOff, updateTakeOff,
    addTakeOffAttachment, updateTakeOffAttachment, removeTakeOffAttachment,
    addRenderSet, addRenderSetRevision, removeRenderSet,
    createQuoteAnalysis, updateQuoteAnalysis, removeQuoteAnalysis, reviseQuoteAnalysis, applyQuoteMarginToAll, finalizeQuoteAnalysis, convertQuoteToScopes, loadDemoQuoteAnalysis,
    updateCloseout, setCloseoutItem, addCloseoutItem, removeCloseoutItem,
    addCloseoutPhotos, updateCloseoutPhoto, removeCloseoutPhoto,
    addCloseoutDoc, updateCloseoutDoc, removeCloseoutDoc,
    addLesson, updateLesson, removeLesson,
    setCloseoutPortfolio, closeProject, reopenCloseout,
    addQuoteSection, updateQuoteSection, removeQuoteSection,
    addQuoteLine, updateQuoteLine, removeQuoteLine,
    addLibraryDocument, removeLibraryDocument, updateLibraryDocument,
addQuoteRevision, updateQuoteRevision, createQuoteFromAnalysis, submitQuoteRevision, updateQuoteRevisionDeck, convertQuoteToContract, undoConvertQuoteToContract, addFollowUp, addChangeOrder, setChangeOrderStatus, updateChangeOrder, undoChangeOrderApproval,
    setPaymentTermStatus, addPaymentRequisition, setPaymentRequisitionStatus, updatePaymentRequisition,
    addVendorEstimate, approveVendor, setPOTermStatus, addVendorEstimateRevision, updateVendorEstimateRevision, updatePurchaseOrder, updateProformaInvoice,
    canEditPOPI: canEditPOPI(currentRole),
    addPurchaseOrderRevision, addFreightPORevision, setPurchaseOrderStatus,
    convertPOToPI, updatePiMaterialLines, addProformaInvoiceRevision, setPIStatus, approvePIForPayment, piExceedsApprovedPO,
    submitFieldForCloseout, closeOutFieldRecord, setFieldRequester,
    salesApproveApInvoice, canSalesApprove,
    postponePaymentTerm, postponeApInvoiceDue,
    notifications, notificationPrefs, emailOutbox, notify, projectWatchers,
    shares, shareItem, loginLog, setQuoteChasePaused, fileOutgoingEmail,
    // Both of these live inside App(), so a softwares/ module cannot reach them
    // directly. They are the two the modules genuinely need: one to mutate a
    // project, one to record that it happened.
    updateProject, logAction,
    officeDocs, setOfficeDocs,
    ctPriceLists, setCtPriceLists,
    fenestrationLibrary,
    softwareSettings, setSoftwareSettings,
    leadTimeRevisions, saveLeadTimeRevision, leadTimePendingChanges,
    scratchProjects, myScratchProject, toolProjects, scratchContents, moveScratchWork,
    exportToSheet,
    doorLibrary, setDoorLibrary, slabs, setSlabs, remnants, setRemnants,
    surfaceLibrary, setSurfaceLibrary,
    markNotificationRead, markAllNotificationsRead, clearReadNotifications,
    setMyNotificationPrefs, setNotificationChannel, markOutboxSent,
    recordClientReceipt, setBankHold, recordBankRelease, undoBankRelease, postponeBankRelease,
    cashEntries, addCashEntry, updateCashEntry, removeCashEntry, updateMiscInvoice, voidApInvoice, setPaymentTermPlan, recordPaymentTermReceipt,
    cashSettings, updateCashSettings, setWeekOpeningBalance, creditCards, addCreditCard, updateCreditCard, removeCreditCard,
    addTask, setTaskStatus, addIssue, setIssueStatus, updateIssue, respondToIssue, addIssueAttachment, removeIssueAttachment,
    addMeeting, updateMeeting, removeMeeting, addMeetingAttachment, removeMeetingAttachment,
    addJobsiteVisit, updateJobsiteVisit, removeJobsiteVisit, addJobsiteVisitPicture,
    reassignTeamRole, updateNotes, addManualLog, updateContact, addAdditionalContact, updateAdditionalContact, removeAdditionalContact,
    updateProjectInfo, updateScopeQuantity, moveProjectToAccount,
    requestProjectDeletion, cancelProjectDeletion, decideProjectDeletion,
    addPaymentTerm, removePaymentTerm, updatePaymentTerm, setRetainagePct,
    addDelivery, removeDelivery, updateDelivery,
    requestDelivery, approveDeliveryRequest, scheduleDelivery, rejectDeliveryRequest, addDeliveryLine, removeDeliveryLine, generateDeliveryPackingList,
    addDeliveryPicture, completeDelivery,
    canApproveDelivery: canApproveDelivery(currentRole),
    canEditDeliveryStatus: canEditDeliveryStatus(currentRole),
    addExportDocument, removeExportDocument, updateExportDocument,
    exportContainers, addExportContainer, updateExportContainer, setContainerDocument, handoffContainerToLogistics,
    logisticsClaims, addLogisticsClaim, updateLogisticsClaim, setLogisticsClaimStatus,
    trucks, addTruck, updateTruck,
    canSeeLogistics: canSeeLogistics(currentRole),
    canSeeProductionTimeline: canSeeProductionTimeline(currentRole),
    canSeeWorkload: canSeeWorkload(currentRole),
    canSeeChangeLog: canSeeChangeLog(currentRole),
    salesTaxRates, updateSalesTaxRate, resetSalesTaxRate,
    tariffLibrary: allTariffClassifications, tariffLines, addTariffClassification, addTariffVersion, updateTariffClassification,
    addTariffLine, updateTariffLine, recordTariffActuals,
    quoteRecipes, setQuoteRecipes, quoteSpecs, setQuoteSpecs,
    quoteSpecImages, setQuoteSpecImages,
    quoteTerms, setQuoteTerms,
    quoteArt, setQuoteArt,
    signEnvelopes, setSignEnvelopes, signCounter, setSignCounter,
    addSignEnvelope, updateSignEnvelope, removeSignEnvelope, sendSignEnvelope,
    recordSignSignature, declineSignEnvelope, voidSignEnvelope, signMarkViewed,
    canSeeTradeCompliance: canSeeTradeCompliance(currentRole),
    // Was defined in data.jsx and called from nowhere — so the toggle existed
    // in the permission matrix and gated nothing, which is worse than having no
    // toggle at all: an admin turning it off believed they had restricted
    // something. Now it is the answer the Document Library actually reads.
    canEditDocLibrary: canEditDocLibrary(currentRole),
    canEditTariffClassification: canEditTariffClassification(currentRole),
    canEditTariffShipmentInfo: canEditTariffShipmentInfo(currentRole),
    addFreightEstimate, updateFreightEstimate, addFreightEstimateRevision, updateFreightEstimateRevision, updateFreightPO, approveFreight,
    addInstallationRecord, updateInstallationRecord, setInstallationStatus,
    rescheduleInstallation, approveInstallationSchedule, requestInstallationReschedule, logInstallationCompletion,
    addDailyFieldReport, addFieldIssue, setFieldIssueStatus, addMaterialReceipt,
    addPunchItem, setPunchStatus, respondToPunchItem, assignPunchItem, schedulePunchReturn, decidePunchResponse,
    setFieldMeasurementNA,
    addFieldMeasurementThread, addFieldMeasurementRevision,
    addVendor, updateVendorTerms, updateVendor, updateVendorBilling, addVendorContact, updateVendorContact, removeVendorContact,
    addFreightForwarder, updateFreightForwarderTerms, updateFreightForwarder, updateFreightForwarderBilling, addFreightForwarderContact,
    subcontractors, addSubcontractor, updateSubcontractor, updateSubcontractorBilling, setSubcontractorActive, linkSubcontractorProject, unlinkSubcontractorProject,
    submitSubcontractorRegistration, approveSubcontractorRegistration, requestSubcontractorChanges, requestSubcontractorEdit, releaseSubcontractorForEditing,
    addAccountAttachment, removeAccountAttachment, addAccountContact, removeAccountContact, addVendorAttachment, removeVendorAttachment,
    addFreightForwarderAttachment, removeFreightForwarderAttachment, addSubcontractorAttachment, removeSubcontractorAttachment,
    addVendorCatalogEntry, removeVendorCatalogEntry, addFreightForwarderCatalogEntry, removeFreightForwarderCatalogEntry,
    addVendorPriceListEntry, removeVendorPriceListEntry, addFreightForwarderPriceListEntry, removeFreightForwarderPriceListEntry,
    canSeeVendorPricing: canSeeVendorPricing(currentRole),
    addApInvoice, addFreightInvoice, addMiscInvoice, setApInvoiceApproval, addApPayment, setApInvoicePaymentStatus,
    addFinancialIssue, setFinancialIssueStatus,
    addUser, updateUser, setUserActive,
    applySovSync, removeSovCategory, addSovItem, updateSovItem, removeSovItem,
    createApplication, updateApplicationLine, updateApplicationCoLine, updateApplicationMeta,
    updateAiaApplicationHeader, saveAiaHeaderDefaults,
    setAppRetainageReleased, setAppPayment,
    markAiaApplicationStage, certifyApplication, reopenApplication, voidApplication, createArFromApplication,
    canApproveAiaApplication: canApproveAiaApplication(currentRole),
    addProductionRecord, setProductionRecordStatus, updateProductionRecord,
    addProductionDrawing, addProductionPhoto, addProductionQcReport,
    addMaterial, updateMaterial, setMaterialActive, addMaterialDocument, removeMaterialDocument,
    addApplianceSpec, updateApplianceSpec, setApplianceSpecActive, addApplianceSpecDocument,
    addFixtureSpec, updateFixtureSpec, setFixtureSpecActive, addFixtureSpecDocument,
    addApplianceInstance, removeApplianceInstance, addFixtureInstance, removeFixtureInstance,
    addInventoryTransaction, receiveInventory, adjustStock, addMaterialAllocation, receiveContainerMaterials, releaseAllocation, cancelAllocation, reallocateMaterial,
    requestStockConversion, decideStockConversion,
    addWarehouseMaterial, updateWarehouseMaterial, markAllocationDelivered,
    createWarehouseRelease, generatePackingList, createDeliveryFromPackingList, confirmDeliveryReceived,
    addSubmittal, addSubmittalRevision, addClientResponse, setScopeMaterialLink,
    addWindowApprovalThread, logWindowRevision, linkWindowNodeRecord, addWindowDeliveryPhase, linkWindowPhaseRecord,
    updateScopeProfitability, updateScopeCostField, lockScopeBaseline, syncScopeActualCostsFromSystem,
    lib,
    goProject: (id) => { setSelectedProjectId(id); setPendingProjectNav(null); setView('project'); },
    goProjectTab: (id, tab, subtab) => { setSelectedProjectId(id); setPendingProjectNav({ tab, subtab: subtab || null }); setView('project'); },
    goDashboard: () => setView('dashboard'),
    goAccounts: () => setView('accounts'),
    goSales: () => setView('sales'),
    goAccountDetail: (id) => { setSelectedAccountId(id); setView('accountDetail'); },
    goAdmin: () => setView('admin'),
    goLeonLibrary: section => { if (section) setHubSection('leonLibrary', section); setView('leonLibrary'); },
    hubSections, setHubSection, bootQuoteId, clearBootQuote: () => setBootQuoteId(null),
    goVendors: () => setView('vendors'),
    goWarehouse: () => setView('warehouse'),
    goLogistics: () => setView('logistics'),
    goTradeCompliance: (subtab) => { setPendingTradeComplianceNav(subtab || null); setView('tradeCompliance'); },
    goLeonStudio: () => setView('leonStudio'),
    // Both of these used to be their own destinations. They are kept, and
    // redirect, because the last view is restored from sessionStorage — a
    // session open on the old key must still land somewhere real.
    goSoftwares: () => setView('leonStudio'),
    // Open LEON Studio ON a named tool. The hub already tracks which section it
    // has open (`hubSections.leonStudio`), which is the same mechanism the boot
    // param uses — so this is the existing route, not a second one.
    goSoftware: (key, params) => {
      // LEON Office is not a LEON_SOFTWARE_LINKS entry — it is its own state
      // inside LeonStudioView — so it is reached by a boot param rather than a
      // section key. 'office:slides' opens Presentation.
      if (key && String(key).indexOf('office') === 0) {
        const app = String(key).split(':')[1] || '';
        if (typeof swBootSet === 'function') swBootSet('office', app);
        setHubSection('leonStudio', '');
      } else if (key) setHubSection('leonStudio', key);
      // A deep link into the tool itself (an envelope, a quote) travels the way
      // every other one does — through the boot-param store the tool reads on
      // mount, never through state the next render would wipe.
      if (params && typeof swBootSet === 'function') {
        Object.keys(params).forEach(k => swBootSet(k, params[k]));
      }
      setView('leonStudio');
    },
    goOffice: () => setView('leonStudio'),
    goMeetTheTeam: () => setView('meetTheTeam'),
    goVendorDetail: (id, type) => { setSelectedVendorId(id); setSelectedVendorType(type || 'vendor'); setView('vendorDetail'); },
    goSubcontractorDetail: (id) => { setSelectedVendorId(id); setSelectedVendorType('subcontractor'); setView('vendorDetail'); },
    goUsers: () => setView('users'),
    goReports: () => { setPendingReportNav(null); setView('reports'); },
    goReport: (reportId, filters) => { setPendingReportNav({ reportId, filters: filters || {} }); setView('reports'); },
    goCalendar: () => setView('calendar'),
    goAccounting: () => setView('accounting'),
    goInbox: () => setView('inbox'),
    goAboutUs: () => setView('aboutUs'),
    goTraining: () => { setHubSection('leonLibrary', 'training'); setView('leonLibrary'); },
    // Map is a subtab of Accounts now; the route is kept so an old saved
    // sessionStorage nav state still resolves instead of rendering nothing.
    goMap: () => setView('accounts'),
  };
  // Point the share registry at the live ctx, the same way the permission
  // registry is re-pointed — so FileField (and anything else without ctx) can
  // offer Share from wherever it is used.
  setActiveShareCtx(ctx);

  const selectedProject = projects.find(p => p.id === selectedProjectId);

  // Nothing is shown until the session has actually been checked. Rendering the
  // login form first would flash it at someone who IS signed in; rendering the
  // app first would show the shell to someone who is not.
  if (authChecking) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-[var(--leon-cream)]">
        <div className="text-center">
          <img src="logo/leon-mark.svg" alt="LEON" className="h-20 w-auto mx-auto mb-3 opacity-80" />
          <p className="text-xs text-[var(--leon-black)]/45">Checking your session…</p>
        </div>
      </div>
    );
  }
  // ARRIVING FROM A RESET EMAIL.
  //
  // This is checked BEFORE the sign-in gate, and the order is the whole point.
  // supabase-js consumes the token in the URL and establishes a session, so
  // without this the person would be signed straight into the app with no way
  // to set a password — and "Change my password" is no help to them, because
  // it asks for the current one, which is exactly what they do not have.
  if (recoveryMode) {
    return <SetNewPasswordScreen
      onDone={(email) => {
        setRecoveryMode(false);
        setLoginError('');
        // Signed out on purpose: the new password should be used at least once
        // straight away, which is what turns "I set it" into "I know it".
        if (typeof leonAuthSignOut === 'function') leonAuthSignOut();
        setAuthedUserId(null);
        setRecoveryDone(email || true);
      }}
      onCancel={() => { setRecoveryMode(false); if (typeof leonAuthClearRecoveryUrl === 'function') leonAuthClearRecoveryUrl(); }} />;
  }

  if (!authedUserId) {
    // NO "Continue as…" and no biometric shortcut here, deliberately.
    //
    // That path set authedUserId from a local id with no password and no token
    // — a complete bypass of the sign-in it sat beside. It had also been dead
    // for its whole life: loadAuthSession() returns the id as a STRING, so
    // `(loadAuthSession() || {}).userId` was always undefined and the panel
    // never rendered. Dead code that grants access if anyone ever "fixes" the
    // line is worse than no feature, so it is gone rather than repaired.
    //
    // Being remembered between visits is Supabase's job now: it keeps and
    // refreshes the session itself, and the effect above restores it. Face ID
    // can come back as a lock ON TOP of a verified session — which is what it
    // honestly is — but not as a way to obtain one.
    return <LoginScreen onLogin={login} error={loginError} onClearError={() => setLoginError('')}
      notice={recoveryDone
        ? ('Password set' + (typeof recoveryDone === 'string' ? ' for ' + recoveryDone : '')
           + '. Sign in with it now.')
        : ''}
      remembered={null}
      onForget={() => { clearAuthSession(); setLoginError(''); setAuthedUserId(null); forceRerender(n => n + 1); }} />;
  }

  // Subcontractors never see the normal app shell — a dedicated, restricted
  // portal only, per explicit requirement (no company P&L, vendor costs,
  // other subcontractors' financials, or any other confidential accounting).
  if (currentRole === 'Subcontractor') {
    return <SubcontractorPortal ctx={ctx} />;
  }
  // Delivery Drivers are internal team members (unlike Subcontractor, no
  // separate company/registration collection) but still get a dedicated,
  // restricted portal — their own scheduled deliveries and the jobsite info
  // needed to run them, nothing else.
  if (currentRole === 'Delivery Driver') {
    return <DeliveryDriverPortal ctx={ctx} />;
  }
  // Clients get a dedicated, restricted, view-only portal — their own jobs'
  // stage progress, shop drawing records, contract overview, receivables,
  // and upcoming deliveries/installations. Never the normal app shell.
  if (currentRole === 'Client') {
    return <ClientPortal ctx={ctx} />;
  }
  // Third-party quality control inspectors get their own narrow view: the
  // inspections booked in their name, and nothing else in the app.
  if (currentRole === 'QC Inspector') {
    return <QCPortal ctx={ctx} />;
  }

  return (
    <div className="min-h-screen">
      <Header ctx={ctx} view={view} />
      {opError && (
        <div className="max-w-7xl mx-auto px-4 md:px-6 mt-2">
          <div className="rounded-md border border-[var(--leon-red)] bg-[var(--leon-red)]/10 px-3 py-2 text-sm flex items-start gap-2">
            <span aria-hidden="true">⚠️</span>
            <span className="flex-1"><strong>Nothing was created.</strong> {opError}</span>
            <button type="button" onClick={() => setOpError('')}
              className="text-xs font-semibold underline">Dismiss</button>
          </div>
        </div>
      )}
      {/* A conflict means somebody else edited the same record while this
          browser held an older copy of it, and the write was refused rather
          than allowed to overwrite them. Their version is what is stored; this
          screen is the stale one, so the honest instruction is to reload.
          Offering "keep mine" here would be offering to discard a colleague's
          work on the strength of a banner. */}
      {syncConflictLabels.length > 0 && (
        <div className="max-w-7xl mx-auto px-4 md:px-6 mt-2">
          <div className="rounded-md border border-[var(--leon-yellow)] bg-[var(--leon-yellow)]/10 px-3 py-2 text-sm flex items-start gap-2">
            <span aria-hidden="true">🔄</span>
            <span className="flex-1">
              <strong>Someone else changed this while you had it open.</strong>{' '}
              Your edit to {syncConflictLabels.join(', ')}{' '}
              {syncConflictLabels.length === 1 ? 'was' : 'were'} not saved, because saving
              would have overwritten theirs. Reload to pick up their version, then make
              your change again.
            </span>
            <button type="button" onClick={() => window.location.reload()}
              className="text-xs font-semibold underline whitespace-nowrap">Reload</button>
          </div>
        </div>
      )}
      {/* The whole current screen is printable, whatever screen it is — the
          per-section buttons below narrow it down, but there is always a way
          to take the page you are looking at as a document. */}
      <main className="max-w-7xl mx-auto px-4 md:px-6 py-6 hub-wrap" data-print-region>
        {view !== 'project' && (
          <div className="hub-tools-parent no-print flex items-center justify-end gap-1 -mt-3 mb-2">
            <ExpandCollapseAll />
            <span className="w-px h-5 bg-[var(--leon-line)] mx-1" />
            <DocActions title={VIEW_PRINT_LABELS[view] || 'LEON Operations Hub'}
              heading={VIEW_PRINT_LABELS[view] || 'LEON Operations Hub'}
              lines={[ctx.activeDepartment !== ALL_DEPARTMENTS ? `${ctx.activeDepartment} department` : null].filter(Boolean)} />
          </div>
        )}
        {view === 'dashboard' && (
          <Dashboard ctx={ctx} filter={dashboardFilter} setFilter={setDashboardFilter} search={search} setSearch={setSearch} />
        )}
        {view === 'accounts' && <AccountsView ctx={ctx} />}
        {view === 'sales' && <SalesHubView ctx={ctx} />}
        {view === 'accountDetail' && (accounts.find(a => a.id === selectedAccountId) ? <AccountDetailView ctx={ctx} account={accounts.find(a => a.id === selectedAccountId)} /> : <EmptyState text="Account not found." />)}
        {view === 'vendors' && (ctx.canView('vendors') ? <VendorsView ctx={ctx} /> : <LockedNotice />)}
        {view === 'vendorDetail' && selectedVendorType === 'subcontractor' && <SubcontractorDetailView ctx={ctx} subId={selectedVendorId} />}
        {view === 'vendorDetail' && selectedVendorType !== 'subcontractor' && <VendorDetailView ctx={ctx} vendorId={selectedVendorId} type={selectedVendorType} />}
        {view === 'users' && (ctx.canSeeFin ? <UsersView ctx={ctx} /> : <LockedNotice />)}
        {view === 'calendar' && <CalendarHubView ctx={ctx} />}
        {view === 'map' && <AccountsView ctx={ctx} />}
        {view === 'leonLibrary' && <LeonLibraryHub ctx={ctx} />}
        {/* Both keys land in Logistics; `warehouse` opens it on Inventory so a
            restored session, a saved link or ctx.goWarehouse still works. */}
        {view === 'warehouse' && <LogisticsDashboard ctx={ctx} startOn="inventory" />}
        {view === 'logistics' && <LogisticsDashboard ctx={ctx} />}
        {view === 'tradeCompliance' && (ctx.canSeeTradeCompliance ? <TradeComplianceView ctx={ctx} pendingSubtab={pendingTradeComplianceNav} onConsumeSubtab={() => setPendingTradeComplianceNav(null)} /> : <LockedNotice />)}
        {view === 'reports' && <ReportsHubTab ctx={ctx} pendingReportNav={pendingReportNav} onConsumeReportNav={() => setPendingReportNav(null)} />}
        {view === 'meetTheTeam' && <MeetTheTeamView ctx={ctx} />}
        {view === 'aboutUs' && <AboutUsHub ctx={ctx} />}
        {/* HUB Training is a section of LEON Library now; this key still lands
            there so a restored session keeps working. */}
        {view === 'training' && <LeonLibraryHub ctx={ctx} startOn="training" />}
        {view === 'leonStudio' && <LeonStudioView ctx={ctx} />}
        {(view === 'softwares' || view === 'office') && <LeonStudioView ctx={ctx} />}
        {view === 'admin' && <AdminSettings ctx={ctx} />}
        {view === 'accounting' && (ctx.canSeeAccountingHub ? <AccountingHub ctx={ctx} /> : <LockedNotice label="The Accounting hub is restricted to Accounting and Admin." />)}
        {view === 'inbox' && <InboxView ctx={ctx} />}
        {view === 'project' && selectedProject && <ProjectDetail key={selectedProject.id} ctx={ctx} project={selectedProject} pendingNav={pendingProjectNav} />}
        {view === 'project' && !selectedProject && <EmptyState text="Project not found." />}
      </main>
      <StageSuggestionToast ctx={ctx} />
      <AppFooter />
    </div>
  );
}

// The one place the app offers to close a stage for you. It ASKS — a stage
// marked complete without being told is worse than one left open, because
// nobody goes looking for a mistake they never heard about. Dismissing is a
// first-class answer, not a way to postpone the question.
function StageSuggestionToast({ ctx }) {
  const sg = ctx.stageSuggestion;
  if (!sg) return null;
  const project = ctx.projects.find(p => p.id === sg.projectId);
  const scope = sg.scopeId ? (project.scopes || []).find(s => s.id === sg.scopeId) : null;
  return (
    <div className="no-print fixed bottom-4 right-4 z-40 w-[22rem] max-w-[calc(100vw-2rem)] bg-white border border-[var(--leon-brown)] rounded-xl shadow-lg overflow-hidden">
      <div className="px-4 py-3">
        <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-brown)]">Mark this stage complete?</p>
        <p className="text-sm font-bold mt-0.5">{sg.stageName}</p>
        <p className="text-xs text-[var(--leon-black)]/50">
          {project ? project.name : ''}{scope ? ` · ${scope.name}` : ' · Lead, Take-Off & Quotes'}
        </p>
        <p className="text-xs text-[var(--leon-black)]/60 mt-1.5">{sg.because}.</p>
      </div>
      <div className="flex items-center gap-2 px-4 py-2 bg-[var(--leon-cream)] border-t border-[var(--leon-line)]">
        <Button size="sm" onClick={ctx.acceptStageSuggestion}>Mark complete</Button>
        <Button size="sm" variant="ghost" onClick={ctx.dismissStageSuggestion}>Not yet</Button>
        <div className="flex-1" />
        <span className="text-[10px] text-[var(--leon-black)]/35">Completing shifts the dates after it.</span>
      </div>
    </div>
  );
}

// App-wide footer — mounted in the main App shell and both portal shells
// (Subcontractor, Delivery Driver) so it's truly everywhere, per explicit
// request. Deliberately understated (small, low-contrast) rather than a
// prominent brand bar.
function AppFooter() {
  return (
    <footer className="no-print text-center py-6 text-[11px] text-[var(--leon-black)]/40">
      <p>Developed exclusively for LEON Integra - 2026 - Version 00</p>
      <p className="mt-1 text-[10px] text-[var(--leon-black)]/25">Made with Love 🦋</p>
    </footer>
  );
}

// ============================================================================
// Login
// ============================================================================
// Setting a password after arriving from a reset email.
//
// Deliberately does NOT ask for the current password: the emailed token is the
// proof of identity, and the people who need this link are precisely the ones
// who cannot supply an old password.
function SetNewPasswordScreen({ onDone, onCancel }) {
  const [pw, setPw] = useState('');
  const [pw2, setPw2] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [expired, setExpired] = useState(false);

  async function submit(e) {
    if (e) e.preventDefault();
    setErr('');
    if (pw.length < 8) { setErr('Choose a password of at least 8 characters.'); return; }
    if (pw !== pw2) { setErr('The two passwords do not match.'); return; }
    setBusy(true);
    const res = await leonAuthCompleteRecovery(pw);
    setBusy(false);
    if (!res.ok) { setErr(res.error); if (res.expired) setExpired(true); return; }
    onDone(res.email);
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-[var(--leon-cream)] px-4">
      <div className="w-full max-w-sm">
        <img src="logo/leon-mark.svg" alt="LEON" className="h-16 w-auto mx-auto mb-5 opacity-85" />
        <div className="bg-white border border-[var(--leon-line)] rounded-lg p-5">
          <h1 className="text-lg font-bold mb-1">Set your password</h1>
          <p className="text-xs text-[var(--leon-black)]/60 mb-4">
            You came here from a reset email, so we do not need your old password.
            Choose one only you know &mdash; at least 8 characters.
          </p>
          {expired ? (
            <div className="text-sm">
              <p className="text-[var(--leon-red)] mb-3">{err}</p>
              <Button variant="secondary" onClick={onCancel}>Back to sign in</Button>
            </div>
          ) : (
            <form onSubmit={submit}>
              <Field label="New password">
                <TextInput type="password" value={pw} autoFocus
                  onChange={(e) => { setPw(e.target.value); setErr(''); }} />
              </Field>
              <Field label="New password again">
                <TextInput type="password" value={pw2}
                  onChange={(e) => { setPw2(e.target.value); setErr(''); }} />
              </Field>
              {err && <p className="text-xs text-[var(--leon-red)] mt-1 mb-2">{err}</p>}
              <div className="flex items-center gap-2 mt-3">
                <Button type="submit" disabled={busy}>{busy ? 'Setting\u2026' : 'Set password'}</Button>
                <button type="button" onClick={onCancel}
                  className="text-xs underline text-[var(--leon-black)]/60">Cancel</button>
              </div>
            </form>
          )}
        </div>
        <p className="text-[10px] text-center text-[var(--leon-black)]/40 mt-3">
          This link works once. If it fails, ask for another.
        </p>
      </div>
    </div>
  );
}

function LoginScreen({ onLogin, error, onClearError, remembered, onQuickIn, onForget, notice }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [bioBusy, setBioBusy] = useState(false);
  const [bioErr, setBioErr] = useState('');
  const enrolled = remembered ? bioEnrolledFor(remembered.id) : false;

  function unlock() {
    setBioErr(''); setBioBusy(true);
    bioVerify(remembered.id)
      .then(() => onQuickIn(remembered.id))
      .catch(err => setBioErr(err && err.name === 'NotAllowedError'
        ? 'Not confirmed — try again, or sign in with your password.'
        : (err && err.message) || 'Could not confirm.'))
      .then(() => setBioBusy(false), () => setBioBusy(false));
  }

  // Awaited now: the password goes to a server, so there is a real wait here and
  // the button has to stay disabled through it. Clearing `submitting` before the
  // promise settled would let someone press Sign In repeatedly and fire several
  // attempts at once — each one counting against the account.
  async function submit(e) {
    e.preventDefault();
    if (!username.trim() || !password) return;
    setSubmitting(true);
    try { await onLogin(username, password); }
    finally { setSubmitting(false); }
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-[var(--leon-cream)] px-4">
      <div className="w-full max-w-sm">
        <div className="flex flex-col items-center mb-6">
          <img src="logo/leon-mark.svg" alt="LEON" className="h-28 w-auto rounded-md mb-3" />
          <span className="block font-brand font-bold text-lg leading-none">LEON</span>
          <span className="block text-[10px] tracking-[0.2em] text-[var(--leon-brown)] leading-none mt-1">OPERATIONS HUB</span>
        </div>
        {/* The standing notice sits ABOVE the form on purpose: it is something
            to be read before signing in, not discovered after. Wording lives in
            SIGNIN_NOTICE (data.jsx), so it is changed in one place. */}
        <NoticeCard notice={SIGNIN_NOTICE} className="mb-4" />
        {/* Confirmation after a password reset. Above the form, because the
            next thing to do is sign in with the password just set. */}
        {notice && (
          <div className="mb-4 rounded-md border border-[var(--leon-green)] bg-[var(--leon-green)]/10 px-3 py-2 text-xs flex items-start gap-2">
            <span aria-hidden="true">&#10003;</span>
            <span>{notice}</span>
          </div>
        )}
        {/* A remembered account short-circuits the password. The session was
            already kept in this browser; this only decides whether the person
            in front of it is asked to confirm. */}
        {remembered && (
          <div className="bg-white border border-[var(--leon-line)] rounded-xl p-5 mb-4 shadow-sm text-center">
            <Avatar name={remembered.name} url={remembered.photoUrl} size={48} />
            <div className="font-bold mt-2">{remembered.name}</div>
            <div className="text-[12px] text-[var(--leon-black)]/55 mb-3">{remembered.username}</div>
            {enrolled ? (
              <>
                <Button className="w-full" onClick={unlock} disabled={bioBusy}>
                  {bioBusy ? 'Waiting for confirmation…' : 'Unlock with Face ID / Touch ID'}
                </Button>
                {bioErr && <p className="text-[12px] text-[var(--leon-red)] mt-2">{bioErr}</p>}
              </>
            ) : (
              <Button className="w-full" onClick={() => onQuickIn(remembered.id)}>Continue as {remembered.name.split(' ')[0]}</Button>
            )}
            <button onClick={onForget}
              className="text-[12px] text-[var(--leon-brown)] hover:underline mt-3">Not you? Sign in as someone else</button>
          </div>
        )}
        <form onSubmit={submit} className="bg-white border border-[var(--leon-line)] rounded-xl p-6 space-y-4 shadow-sm">
          <h1 className="text-lg font-bold text-center mb-1">{remembered ? 'Sign in as someone else' : 'Sign In'}</h1>
          <Field label="Username">
            <TextInput
              autoFocus
              value={username}
              onChange={e => { setUsername(e.target.value); if (error) onClearError(); }}
              placeholder="e.g. imarchetti"
            />
          </Field>
          <Field label="Password">
            <TextInput
              type="password"
              value={password}
              onChange={e => { setPassword(e.target.value); if (error) onClearError(); }}
              placeholder="••••••••"
            />
          </Field>
          {error && <p className="text-xs font-semibold text-[var(--leon-red)]">{error}</p>}
          <Button type="submit" className="w-full" disabled={submitting || !username.trim() || !password}>
            {submitting ? 'Signing in…' : 'Sign In'}
          </Button>
          {/* The username rule is genuinely useful and stays. The PASSWORD used
              to be printed here beside the words "Demo credentials" — which
              made a live business system read as a demonstration, and published
              the one password every account shares to anyone who opened the
              page. Neither belongs on a sign-in screen. */}
          <p className="text-[11px] text-[var(--leon-black)]/45 text-center pt-1">
            Your username is the part of your @leonintegra.com email before the @.
          </p>
        </form>
      </div>
    </div>
  );
}

// ============================================================================
// Header / Nav
// ============================================================================
// ============================================================================
// Subcontractor Portal (§5-7) — the ONLY view a Subcontractor-role login can
// reach. Deliberately does not import/use any company-wide financial data
// (profitability, vendor costs, other vendors/subcontractors, P&L, margins)
// — it only ever reads data scoped to this one subcontractor and their own
// linked projects.
// ============================================================================
function SubcontractorPortal({ ctx }) {
  // Prefer the real id link (set at account creation); fall back to the
  // legacy email match only for logins created before that link existed.
  const sub = (ctx.currentUser.subcontractorId && ctx.subcontractors.find(s => s.id === ctx.currentUser.subcontractorId))
    || ctx.subcontractors.find(s => s.email === ctx.currentUser.email);
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);
  const [installProjectId, setInstallProjectId] = useState('');

  if (!sub) {
    return (
      <div className="min-h-screen flex items-center justify-center p-6">
        <div className="text-center">
          <p className="font-bold mb-2">No subcontractor account is linked to this login.</p>
          <button onClick={ctx.logout} className="text-sm text-[var(--leon-brown)] font-semibold">Log Out</button>
        </div>
      </div>
    );
  }

  const myProjects = sub.projectIds.map(id => ctx.projects.find(p => p.id === id)).filter(Boolean);
  const allInvoices = useMemo(() => allApInvoices(ctx.projects), [ctx.projects]);
  const myInvoices = allInvoices.filter(i => i.vendorId === sub.id);
  const installProject = myProjects.find(p => p.id === installProjectId) || myProjects[0] || null;

  return (
    <div className="min-h-screen">
      <header className="no-print border-b border-[var(--leon-line)] bg-white sticky top-0 z-30">
        <div className="max-w-5xl mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-4 flex-wrap">
          <div className="flex items-center gap-2.5">
            <img src="logo/leon-mark.svg" alt="LEON" className="h-[4.5rem] w-auto rounded-sm" />
            <span>
              <span className="block font-brand font-bold text-sm leading-none">LEON</span>
              <span className="block text-[9px] tracking-[0.2em] text-[var(--leon-brown)] leading-none mt-0.5">SUBCONTRACTOR PORTAL</span>
            </span>
          </div>
          <div className="flex items-center gap-3 text-sm">
            <PortalInbox ctx={ctx} />
            <div className="flex flex-col items-end">
              <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Signed in as</span>
              <span className="text-xs font-semibold">{sub.contactName} — {sub.companyName}</span>
            </div>
            <Avatar name={sub.contactName} size={32} />
            <button onClick={ctx.logout} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">Log Out</button>
          </div>
        </div>
      </header>

      <main className="max-w-5xl mx-auto px-4 md:px-6 py-6">
        <h1 className="text-2xl font-bold mb-1">Welcome, {sub.contactName}</h1>
        <p className="text-sm text-[var(--leon-black)]/50 mb-6">{sub.trade} · {sub.companyName}</p>

        <Collapsible title="My Projects" count={myProjects.length}>
          {myProjects.length === 0 ? <EmptyState text="No projects assigned yet." /> : (
            <div className="space-y-3">
              {myProjects.map(p => (
                <div key={p.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <p className="font-bold text-sm">{p.name}</p>
                  <p className="text-xs text-[var(--leon-black)]/50">{p.address}</p>
                  <div className="mt-2 space-y-2">
                    {p.scopes.map(scope => {
                      const approvedDrawings = scope.submittals.filter(t => ['Approved', 'Approved as Noted'].includes(t.status));
                      const activeStage = scope.stages.find(st => st.status === 'In Progress');
                      return (
                        <div key={scope.id} className="border-t border-[var(--leon-line)] pt-2">
                          <p className="text-sm font-semibold">{scope.name}</p>
                          <p className="text-xs text-[var(--leon-black)]/50">Current stage: {activeStage ? `${activeStage.name} (due ${fmtDate(activeStage.plannedDue)})` : '—'}</p>
                          {approvedDrawings.length > 0 && (
                            <div className="mt-1">
                              <span className="text-[11px] font-semibold text-[var(--leon-black)]/50 uppercase">Approved Drawings: </span>
                              {approvedDrawings.map(t => {
                                const latest = latestProductionDoc(t.revisions);
                                return latest ? <FileField key={t.id} name={latest.file} url={latest.fileUrl} editable={false} onChange={() => {}} /> : null;
                              })}
                            </div>
                          )}
                        </div>
                      );
                    })}
                  </div>
                </div>
              ))}
            </div>
          )}
        </Collapsible>

        <Collapsible title="Installation">
          {myProjects.length === 0 ? <EmptyState text="No projects assigned yet." /> : (
            <>
              {myProjects.length > 1 && (
                <div className="flex items-center gap-2 mb-3">
                  <span className="text-xs font-semibold text-[var(--leon-black)]/50">Project:</span>
                  <Select value={installProject ? installProject.id : ''} onChange={e => setInstallProjectId(e.target.value)} className="!w-64 !py-1 !text-xs">
                    {myProjects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                  </Select>
                </div>
              )}
              {installProject && <InstallationTab ctx={ctx} project={installProject} />}
            </>
          )}
        </Collapsible>

        <Collapsible title="My Invoices" count={myInvoices.length} right={<Button size="sm" onClick={() => setAddInvoice(true)}>+ Submit Invoice</Button>}>
          {myInvoices.length === 0 ? <EmptyState text="No invoices submitted yet." /> : (
            <div className="overflow-x-auto">
              <table className="w-full text-xs">
                <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3">Invoice #</th><th className="py-1 pr-3">Project</th><th className="py-1 pr-3">Amount</th><th className="py-1 pr-3">Approval Status</th><th className="py-1 pr-3">Payment Status</th><th className="py-1 pr-3">Open Balance</th></tr></thead>
                <tbody>
                  {myInvoices.map(inv => (
                    <tr key={inv.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => setOpenInvoice(inv.id)}>
                      <td className="py-1.5 pr-3 font-semibold text-[var(--leon-brown)]">{inv.invoiceNumber}</td>
                      <td className="py-1.5 pr-3">{inv.projectName}</td>
                      <td className="py-1.5 pr-3">{fmtMoney(inv.amount)}</td>
                      <td className="py-1.5 pr-3"><StatusBadge status={inv.approvalStatus} /></td>
                      <td className="py-1.5 pr-3"><StatusBadge status={inv.paymentStatus} /></td>
                      <td className="py-1.5 pr-3">{fmtMoney(invoiceOpenBalance(inv))}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </Collapsible>

        <Collapsible title="My Company" right={<StatusBadge status={sub.registrationStatus} />}>
          <SubcontractorRegistrationForm ctx={ctx} sub={sub} />
        </Collapsible>

        <p className="text-[11px] text-[var(--leon-black)]/30 mt-6">This portal shows only your own account, assigned projects, and invoices — you don't have access to company financials, other vendors, or other subcontractors' information.</p>
      </main>

      <AddSubcontractorInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} subcontractor={sub} />
      <SubPortalInvoiceViewModal invoiceId={openInvoice} invoices={myInvoices} onClose={() => setOpenInvoice(null)} />
      <AppFooter />
    </div>
  );
}
// Subcontractor self-registration workflow (§ registration request): the
// company fills this out like a registration form including W9/COI, submits
// it, Accounting approves it, and it locks — editable again only once
// Accounting either sends it back for changes or the subcontractor requests
// edit access and Accounting releases it. `editable` collapses to one flag
// so the whole form (fields, billing, uploads) is consistently either fully
// open or fully locked — never partially editable.
function SubcontractorRegistrationForm({ ctx, sub }) {
  const [showEditRequest, setShowEditRequest] = useState(false);
  const editable = sub.registrationStatus === 'Draft';
  const canSubmit = editable && sub.companyName.trim() && sub.contactName.trim() && sub.w9FileUrl && sub.coiFileUrl;

  return (
    <div>
      {sub.registrationStatus === 'Submitted' && (
        <div className="mb-3 bg-[var(--leon-cream)] border border-[var(--leon-brown)] rounded-lg p-3">
          <p className="text-sm font-semibold text-[var(--leon-brown)]">Submitted for approval on {fmtDate(sub.submittedDate)}.</p>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Your information is locked while Accounting reviews it. You'll be able to edit again if changes are requested.</p>
        </div>
      )}
      {sub.registrationStatus === 'Approved' && (
        <div className="mb-3 bg-[#e8f5e9] border border-[var(--leon-green)] rounded-lg p-3 flex items-center justify-between gap-3 flex-wrap">
          <div>
            <p className="text-sm font-semibold text-[var(--leon-green)]">Approved by {sub.approvedBy} on {fmtDate(sub.approvedDate)}.</p>
            <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Your information is locked. Request edit access if something needs to change.</p>
          </div>
          <Button size="sm" variant="outline" onClick={() => setShowEditRequest(true)}>Request to Edit</Button>
        </div>
      )}
      {sub.registrationStatus === 'Edit Requested' && (
        <div className="mb-3 bg-[var(--leon-cream)] border border-[var(--leon-brown)] rounded-lg p-3">
          <p className="text-sm font-semibold text-[var(--leon-brown)]">Edit access requested on {fmtDate(sub.editRequestedDate)}.</p>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{sub.editRequestNote || 'Waiting on Accounting to release your information for editing.'}</p>
        </div>
      )}
      {sub.registrationStatus === 'Draft' && sub.registrationHistory.length > 0 && (
        <div className="mb-3 bg-[var(--leon-cream)] border border-[var(--leon-line)] rounded-lg p-3">
          <p className="text-sm font-semibold">Open for editing — resubmit when ready.</p>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{sub.registrationHistory[sub.registrationHistory.length - 1].notes || 'Update your information below and submit again for approval.'}</p>
        </div>
      )}

      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Company Name"><TextInput disabled={!editable} value={sub.companyName} onChange={e => ctx.updateSubcontractor(sub.id, { companyName: e.target.value })} /></Field>
          <Field label="Trade / Service"><Select disabled={!editable} value={sub.trade} onChange={e => ctx.updateSubcontractor(sub.id, { trade: e.target.value })}>{SUBCONTRACTOR_TRADES.map(t => <option key={t}>{t}</option>)}</Select></Field>
          <Field label="Main office" hint="Where this company works out of"><Select disabled={!editable} value={officeByKey(sub.officeLocation) ? officeByKey(sub.officeLocation).key : ''} onChange={e => ctx.updateSubcontractor(sub.id, { officeLocation: e.target.value })}><option value="">— not set —</option>{LEON_OFFICES.map(o => <option key={o.key} value={o.key}>{o.flag} {o.city}, {o.country}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Contact Name"><TextInput disabled={!editable} value={sub.contactName} onChange={e => ctx.updateSubcontractor(sub.id, { contactName: e.target.value })} /></Field>
          <Field label="Email"><TextInput disabled={!editable} value={sub.email} onChange={e => ctx.updateSubcontractor(sub.id, { email: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Phone"><TextInput disabled={!editable} value={sub.phone} onChange={e => ctx.updateSubcontractor(sub.id, { phone: e.target.value })} /></Field>
          <Field label="Mobile"><TextInput disabled={!editable} value={sub.mobile} onChange={e => ctx.updateSubcontractor(sub.id, { mobile: e.target.value })} /></Field>
        </div>
        <Field label="Address"><TextInput disabled={!editable} value={sub.address} onChange={e => ctx.updateSubcontractor(sub.id, { address: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="City"><TextInput disabled={!editable} value={sub.city} onChange={e => ctx.updateSubcontractor(sub.id, { city: e.target.value })} /></Field>
          <Field label="State"><TextInput disabled={!editable} value={sub.state} onChange={e => ctx.updateSubcontractor(sub.id, { state: e.target.value })} /></Field>
          <Field label="ZIP Code"><TextInput disabled={!editable} value={sub.zip} onChange={e => ctx.updateSubcontractor(sub.id, { zip: e.target.value })} /></Field>
        </div>
      </div>

      <h3 className="font-bold text-sm mt-4 mb-2">Billing Information</h3>
      <VendorBillingFields billing={sub.billing} onChange={fields => editable && ctx.updateSubcontractorBilling(sub.id, fields)} />

      <h3 className="font-bold text-sm mt-4 mb-2">Required Documents</h3>
      <div className="grid grid-cols-2 gap-3">
        <Field label="W9"><FileField name={sub.w9File} url={sub.w9FileUrl} editable={editable} placeholder="Upload W9" onChange={(fname, url) => ctx.updateSubcontractor(sub.id, { w9File: fname, w9FileUrl: url })} /></Field>
        <Field label="Certificate of Insurance (COI)"><FileField name={sub.coiFile} url={sub.coiFileUrl} editable={editable} placeholder="Upload COI" onChange={(fname, url) => ctx.updateSubcontractor(sub.id, { coiFile: fname, coiFileUrl: url })} /></Field>
      </div>

      {editable && (
        <div className="mt-4">
          <Button onClick={() => ctx.submitSubcontractorRegistration(sub.id)} disabled={!canSubmit}>Submit for Approval</Button>
          {!canSubmit && <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">Company name, contact name, W9, and COI are required before submitting.</p>}
        </div>
      )}

      <RequestSubcontractorEditModal open={showEditRequest} onClose={() => setShowEditRequest(false)} ctx={ctx} sub={sub} />
    </div>
  );
}
function RequestSubcontractorEditModal({ open, onClose, ctx, sub }) {
  const [note, setNote] = useState('');
  useEffect(() => { if (open) setNote(''); }, [open]);
  function submit() { ctx.requestSubcontractorEdit(sub.id, note); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Request Edit Access" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Send Request</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Tell Accounting what needs to change — your information stays locked until they release it for editing.</p>
        <Field label="What needs to change?"><TextArea rows={3} value={note} onChange={e => setNote(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
// A read-only invoice view for the subcontractor portal — no approval or
// payment-status controls (subcontractors submit and view only; they can
// never edit payment records).
function SubPortalInvoiceViewModal({ invoiceId, invoices, onClose }) {
  const invoice = invoiceId ? invoices.find(i => i.id === invoiceId) : null;
  if (!invoice) return null;
  const groupMates = invoice.invoiceGroupId ? invoices.filter(i => i.invoiceGroupId === invoice.invoiceGroupId && i.id !== invoice.id) : [];
  return (
    <Modal open={!!invoice} onClose={onClose} title={`Invoice #${invoice.invoiceNumber}`} footer={<Button variant="ghost" onClick={onClose}>Close</Button>}>
      <div className="space-y-3 text-sm">
        <div className="grid grid-cols-2 gap-3">
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Project</span>{invoice.projectName}</div>
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Amount</span>{fmtMoney(invoice.amount)}</div>
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Approval Status</span><StatusBadge status={invoice.approvalStatus} /></div>
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Payment Status</span><StatusBadge status={invoice.paymentStatus} /></div>
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Amount Paid</span>{fmtMoney(invoiceTotalPaid(invoice))}</div>
          <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Open Balance</span>{fmtMoney(invoiceOpenBalance(invoice))}</div>
        </div>
        {invoice.rejectionReason && <div className="border border-[var(--leon-red)]/40 bg-[#fbe7e7] rounded-md p-2 text-xs text-[var(--leon-red)]">{invoice.rejectionReason}</div>}
        {invoice.description && <p>{invoice.description}</p>}
        {invoice.lines && invoice.lines.length > 0 && (
          <div>
            <span className="text-xs text-[var(--leon-black)]/40 uppercase block mb-1">Line Items — {invoice.projectName}</span>
            <div className="space-y-1">
              {invoice.lines.map((l, i) => <div key={i} className="flex items-center justify-between text-xs border-t border-[var(--leon-line)] pt-1">{l.description || '—'}<span className="font-semibold">{fmtMoney(l.amount)}</span></div>)}
            </div>
          </div>
        )}
        {groupMates.length > 0 && (
          <p className="text-xs text-[var(--leon-brown)]">This invoice also covers: {groupMates.map(g => `${g.projectName} (${fmtMoney(g.amount)})`).join(', ')}</p>
        )}
        {invoice.file && <p><FileField name={invoice.file} url={invoice.fileUrl} editable={false} onChange={() => {}} /></p>}
      </div>
    </Modal>
  );
}

// ============================================================================
// Delivery Driver Hub (§ Material-to-Delivery pipeline) — the ONLY view a
// Delivery Driver-role login can reach. Mirrors SubcontractorPortal's
// dedicated-portal pattern, minus the separate-company/registration bits
// since drivers are internal team members. Shows only this driver's own
// assigned, scheduled deliveries and the jobsite info needed to run them.
// ============================================================================
function DeliveryDriverPortal({ ctx }) {
  const [confirmingId, setConfirmingId] = useState(null);
  const myDeliveries = ctx.projects.flatMap(p => p.deliveries.filter(d => d.driverId === ctx.currentUserId).map(d => ({ ...d, project: p })));
  const active = myDeliveries.filter(d => !['Delivered', 'Cancelled'].includes(d.deliveryStatus)).sort((a, b) => (a.date < b.date ? -1 : 1));
  const completed = myDeliveries.filter(d => ['Delivered', 'Cancelled'].includes(d.deliveryStatus)).sort((a, b) => (a.date < b.date ? 1 : -1));
  const confirming = confirmingId ? myDeliveries.find(d => d.id === confirmingId) : null;

  if (confirming) {
    return <DriverProofOfDeliveryFlow ctx={ctx} delivery={confirming} project={confirming.project} onDone={() => setConfirmingId(null)} />;
  }

  return (
    <div className="min-h-screen">
      <header className="no-print border-b border-[var(--leon-line)] bg-white sticky top-0 z-30">
        <div className="max-w-3xl mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-4 flex-wrap">
          <div className="flex items-center gap-2.5">
            <img src="logo/leon-mark.svg" alt="LEON" className="h-[4.5rem] w-auto rounded-sm" />
            <span>
              <span className="block font-brand font-bold text-sm leading-none">LEON</span>
              <span className="block text-[9px] tracking-[0.2em] text-[var(--leon-brown)] leading-none mt-0.5">DELIVERY DRIVER HUB</span>
            </span>
          </div>
          <div className="flex items-center gap-3 text-sm">
            <PortalInbox ctx={ctx} />
            <div className="flex flex-col items-end">
              <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Signed in as</span>
              <span className="text-xs font-semibold">{ctx.currentUserName}</span>
            </div>
            <Avatar name={ctx.currentUserName} size={32} />
            <button onClick={ctx.logout} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">Log Out</button>
          </div>
        </div>
      </header>

      <main className="max-w-3xl mx-auto px-4 md:px-6 py-6">
        <h1 className="text-2xl font-bold mb-1">Welcome, {ctx.currentUserName}</h1>
        <p className="text-sm text-[var(--leon-black)]/50 mb-6">Your scheduled deliveries and jobsite information.</p>

        <Collapsible title="My Deliveries" count={active.length}>
          {active.length === 0 ? <EmptyState text="No deliveries scheduled to you right now." /> : (
            <div className="space-y-3">
              {active.map(d => <DriverDeliveryCard key={d.id} ctx={ctx} delivery={d} project={d.project} onStart={() => setConfirmingId(d.id)} />)}
            </div>
          )}
        </Collapsible>

        <Collapsible title="Completed" count={completed.length}>
          {completed.length === 0 ? <EmptyState text="Nothing completed yet." /> : (
            <div className="space-y-2">
              {completed.map(d => (
                <div key={d.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-center gap-2 flex-wrap">
                    <Badge tone="black">{d.deliveryNumber}</Badge>
                    <StatusBadge status={d.deliveryStatus} />
                    <span className="text-xs text-[var(--leon-black)]/50">{fmtDate(d.date)} · {d.project.name}</span>
                  </div>
                  {d.cancelReason && <p className="text-xs text-[var(--leon-red)] mt-1">Cancelled: {d.cancelReason}</p>}
                </div>
              ))}
            </div>
          )}
        </Collapsible>

        <p className="text-[11px] text-[var(--leon-black)]/30 mt-6">This portal shows only your own scheduled deliveries and the jobsite information needed to run them — you don't have access to company financials or other projects.</p>
      </main>
      <AppFooter />
    </div>
  );
}
function DriverDeliveryCard({ ctx, delivery, project, onStart }) {
  const contact = project.contacts['Jobsite Delivery Contact'] || { company: '', person: '', phone: '', email: '' };
  const [notes, setNotes] = useState(delivery.driverNotes || '');
  function saveNotes() { if (notes !== delivery.driverNotes) ctx.updateDelivery(project.id, delivery.id, { driverNotes: notes }); }
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-start justify-between gap-2 flex-wrap">
        <div>
          <div className="flex items-center gap-2 flex-wrap">
            <Badge tone="black">{delivery.deliveryNumber}</Badge>
            <StatusBadge status={delivery.deliveryStatus} />
          </div>
          <p className="text-sm font-bold mt-1">{project.name}</p>
          <p className="text-xs text-[var(--leon-black)]/50">{project.address}</p>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{fmtDate(delivery.date)}{delivery.deliveryTime ? ` ${delivery.deliveryTime}` : ''}{delivery.durationMinutes ? ` · ~${delivery.durationMinutes} min` : ''}</p>
          <p className="text-xs text-[var(--leon-black)]/50">Jobsite Contact: {contact.person || '—'}{contact.phone ? ` · ${contact.phone}` : ''}</p>
        </div>
      </div>
      <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Materials</p>
        {delivery.lines.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40 italic">No material lines on this delivery.</p> : (
          <div className="space-y-1">
            {delivery.lines.map(l => <p key={l.id} className="text-xs">{l.quantity} {l.unit} — {l.itemName}</p>)}
          </div>
        )}
      </div>
      <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Photos</p>
        <div className="flex items-center gap-2 flex-wrap">
          {delivery.deliveryPictures.map((url, i) => <ClickableImage key={i} src={url} name={`Photo ${i + 1}`} className="w-14 h-14 object-cover rounded-md" />)}
          <ImagePicker url={null} onChange={url => ctx.addDeliveryPicture(project.id, delivery.id, url)} size={56} />
        </div>
      </div>
      <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} onBlur={saveNotes} /></Field>
      </div>
      <div className="mt-2 flex justify-end">
        <Button size="sm" onClick={onStart}>Start Delivery Confirmation</Button>
      </div>
    </div>
  );
}
// The driver's proof-of-delivery flow — a printable document (mirrors
// AiaPrintPackage's .print-area pattern for an optional Print/Save-as-PDF of
// the finished record) listing every material line with a delivered-quantity
// input, plus a real on-glass SignaturePad. Submitting computes the outcome
// (Complete/Partial/Cancelled) and hands it to ctx.completeDelivery, which is
// the only place allocations actually get released/marked delivered.
function DriverProofOfDeliveryFlow({ ctx, delivery, project, onDone }) {
  const [qtys, setQtys] = useState(() => Object.fromEntries(delivery.lines.map(l => [l.id, String(l.quantity)])));
  const [signatureUrl, setSignatureUrl] = useState(null);
  const [receiverName, setReceiverName] = useState(delivery.receiverName || '');
  const [driverNotes, setDriverNotes] = useState(delivery.driverNotes || '');
  const [cancelling, setCancelling] = useState(false);
  const [cancelReason, setCancelReason] = useState('');
  const [error, setError] = useState('');
  const contact = project.contacts['Jobsite Delivery Contact'] || { company: '', person: '', phone: '', email: '' };

  function submitDelivered() {
    if (!signatureUrl) { setError('Client signature is required to complete this delivery.'); return; }
    const lines = delivery.lines.map(l => ({ id: l.id, deliveredQuantity: Math.max(0, Math.min(Number(qtys[l.id]) || 0, l.quantity)) }));
    const allFull = delivery.lines.every(l => (Number(qtys[l.id]) || 0) >= l.quantity);
    const anyDelivered = lines.some(l => l.deliveredQuantity > 0);
    if (!anyDelivered) { setError('Enter at least one delivered quantity, or use Cancel Delivery instead.'); return; }
    ctx.completeDelivery(project.id, delivery.id, { lines, outcome: allFull ? 'Complete' : 'Partial', driverNotes, clientSignatureUrl: signatureUrl, receiverName });
    onDone();
  }
  function submitCancelled() {
    if (!cancelReason.trim()) { setError('A reason is required to cancel this delivery.'); return; }
    ctx.completeDelivery(project.id, delivery.id, { lines: delivery.lines.map(l => ({ id: l.id, deliveredQuantity: 0 })), outcome: 'Cancelled', cancelReason, driverNotes });
    onDone();
  }

  return (
    <div className="min-h-screen">
      <header className="no-print border-b border-[var(--leon-line)] bg-white sticky top-0 z-30">
        <div className="max-w-2xl mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-4">
          <button onClick={onDone} className="text-sm font-semibold text-[var(--leon-brown)]">← Back</button>
          <Button size="sm" variant="outline" onClick={() => window.print()}>🖨 Print / Save as PDF</Button>
        </div>
      </header>
      <main className="max-w-2xl mx-auto px-4 md:px-6 py-6 print-area">
        <h1 className="text-xl font-bold">Delivery Confirmation — {delivery.deliveryNumber}</h1>
        <p className="text-sm text-[var(--leon-black)]/60 mt-1">{project.name}</p>
        <p className="text-xs text-[var(--leon-black)]/50">{project.address}</p>
        <p className="text-xs text-[var(--leon-black)]/50 mb-4">Jobsite Contact: {contact.person || '—'}{contact.phone ? ` · ${contact.phone}` : ''}</p>

        <table className="w-full text-sm mb-4 border border-[var(--leon-line)] rounded-lg overflow-hidden">
          <thead className="bg-[var(--leon-cream)] text-xs uppercase text-[var(--leon-black)]/50">
            <tr><th className="text-left py-1.5 px-2">Material</th><th className="text-right py-1.5 px-2">Planned</th><th className="text-right py-1.5 px-2">Delivered</th></tr>
          </thead>
          <tbody>
            {delivery.lines.map(l => (
              <tr key={l.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 px-2">{l.itemName}</td>
                <td className="py-1.5 px-2 text-right">{l.quantity} {l.unit}</td>
                <td className="py-1.5 px-2 text-right no-print">
                  <TextInput type="number" className="!w-20 !py-1 !text-xs !text-right" value={qtys[l.id]} onChange={e => setQtys({ ...qtys, [l.id]: e.target.value })} />
                </td>
                <td className="py-1.5 px-2 text-right print-only">{qtys[l.id]} {l.unit}</td>
              </tr>
            ))}
          </tbody>
        </table>

        <div className="no-print">
          <Field label="Receiver Name"><TextInput value={receiverName} onChange={e => setReceiverName(e.target.value)} /></Field>
          <Field label="Driver Notes"><TextArea rows={2} value={driverNotes} onChange={e => setDriverNotes(e.target.value)} /></Field>
        </div>
        {receiverName && <p className="print-only text-sm mb-2">Received by: {receiverName}</p>}

        <p className="text-xs font-semibold mb-1">Client Signature</p>
        <SignaturePad onChange={setSignatureUrl} />
        {signatureUrl && <img src={signatureUrl} alt="Signature" className="print-only h-16 mt-2" />}

        {error && <p className="text-xs font-semibold text-[var(--leon-red)] mt-3">⚠ {error}</p>}

        <div className="no-print mt-4 flex flex-wrap gap-2">
          <Button onClick={submitDelivered}>Complete Delivery</Button>
          <Button variant="ghost" onClick={() => { setCancelling(true); setError(''); }}>Cancel Delivery</Button>
        </div>
        {cancelling && (
          <div className="no-print mt-3 border border-[var(--leon-red)]/30 bg-[#fbe7e7] rounded-lg p-3">
            <Field label="Cancel Reason"><TextArea rows={2} value={cancelReason} onChange={e => setCancelReason(e.target.value)} /></Field>
            <div className="mt-2 flex gap-2">
              <Button variant="danger" size="sm" onClick={submitCancelled}>Confirm Cancel</Button>
              <Button variant="ghost" size="sm" onClick={() => setCancelling(false)}>Back</Button>
            </div>
          </div>
        )}
      </main>
    </div>
  );
}

// Client Portal — view-only, external login for a client contact (§ client
// portal request). Restricted to the account it's linked to (ctx.currentUser.
// accountId, set by addClientPortalLogin) — myProjects below filters to
// exactly that account's jobs, which is what keeps a client from ever
// seeing another client's project. No edit affordances anywhere in this
// tree; it only ever reads ctx data, never calls a mutating ctx function
// (Receivables reuses AccountsReceivableSubTab, whose own edit controls are
// already gated on ctx.canEdit('financials') — false for the Client role,
// see MODULE_EDIT_RIGHTS, data.jsx).
function ClientDeliveryInstallationList({ items, emptyText }) {
  if (items.length === 0) return <EmptyState text={emptyText} />;
  return (
    <div className="space-y-1.5">
      {items.map((u, i) => (
        <div key={i} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs">
          <div>
            <Badge tone={u.type === 'Delivery' ? 'green' : 'blue'}>{u.type}</Badge>
            <span className="font-semibold ml-2">{u.label}</span>
            <p className="text-[var(--leon-black)]/50 mt-0.5">{u.projectName}</p>
          </div>
          <div className="text-right">
            <p className="font-semibold">{fmtDate(u.date)}</p>
            <StatusBadge status={u.status} />
          </div>
        </div>
      ))}
    </div>
  );
}
function ClientPortal({ ctx }) {
  const account = ctx.accounts.find(a => a.id === ctx.currentUser.accountId);
  const [openProjectId, setOpenProjectId] = useState(null);

  if (!account) {
    return (
      <div className="min-h-screen flex items-center justify-center p-6">
        <div className="text-center">
          <p className="font-bold mb-2">No client account is linked to this login.</p>
          <button onClick={ctx.logout} className="text-sm text-[var(--leon-brown)] font-semibold">Log Out</button>
        </div>
      </div>
    );
  }

  const myProjects = ctx.projects.filter(p => p.accountId === account.id);
  const openProject = myProjects.find(p => p.id === openProjectId);

  const today = todayISO();
  const upcoming = [];
  const past = [];
  myProjects.forEach(p => {
    (p.deliveries || []).forEach(d => {
      if (d.deliveryStatus === 'Cancelled') return;
      const item = { type: 'Delivery', date: d.date, label: d.description || 'Delivery', status: d.deliveryStatus, projectName: p.name };
      (d.date >= today ? upcoming : past).push(item);
    });
    (p.installationRecords || []).forEach(r => {
      const label = `${r.building || ''} ${r.room || ''}`.trim() || 'Installation';
      if (r.scheduledStart) {
        const item = { type: 'Installation', date: r.scheduledStart, label, status: r.outcome || 'Scheduled', projectName: p.name };
        (r.scheduledStart >= today ? upcoming : past).push(item);
      }
      if (r.returnVisit && r.returnVisit.date) {
        const item = { type: 'Installation', date: r.returnVisit.date, label: `Return Visit — ${label}`, status: 'Scheduled', projectName: p.name };
        (r.returnVisit.date >= today ? upcoming : past).push(item);
      }
    });
  });
  upcoming.sort((a, b) => a.date.localeCompare(b.date));
  past.sort((a, b) => b.date.localeCompare(a.date));

  return (
    <div className="min-h-screen">
      <header className="no-print border-b border-[var(--leon-line)] bg-white sticky top-0 z-30">
        <div className="max-w-5xl mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-4 flex-wrap">
          <div className="flex items-center gap-2.5">
            <img src="logo/leon-mark.svg" alt="LEON" className="h-[4.5rem] w-auto rounded-sm" />
            <span>
              <span className="block font-brand font-bold text-sm leading-none">LEON</span>
              <span className="block text-[9px] tracking-[0.2em] text-[var(--leon-brown)] leading-none mt-0.5">CLIENT PORTAL</span>
            </span>
          </div>
          <div className="flex items-center gap-3 text-sm">
            <PortalInbox ctx={ctx} />
            <div className="flex flex-col items-end">
              <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Signed in as</span>
              <span className="text-xs font-semibold">{ctx.currentUser.name} — {account.name}</span>
            </div>
            <Avatar name={ctx.currentUser.name} size={32} />
            <button onClick={ctx.logout} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">Log Out</button>
          </div>
        </div>
      </header>

      <main className="max-w-5xl mx-auto px-4 md:px-6 py-6">
        {openProject ? (
          <ClientProjectDetail ctx={ctx} project={openProject} onBack={() => setOpenProjectId(null)} />
        ) : (
          <>
            <h1 className="text-2xl font-bold mb-1">Welcome, {account.contactName || account.name}</h1>
            <p className="text-sm text-[var(--leon-black)]/50 mb-6">{account.name}</p>

            <Collapsible title="Upcoming Deliveries & Installations" count={upcoming.length}>
              <ClientDeliveryInstallationList items={upcoming} emptyText="Nothing scheduled yet." />
            </Collapsible>
            <Collapsible title="Past Deliveries & Installations" count={past.length}>
              <ClientDeliveryInstallationList items={past} emptyText="Nothing delivered or installed yet." />
            </Collapsible>

            <h2 className="text-lg font-bold mt-6 mb-3">My Projects</h2>
            {myProjects.length === 0 ? <EmptyState text="No projects yet." /> : (
              <div className="space-y-3">
                {myProjects.map(p => (
                  <button key={p.id} onClick={() => setOpenProjectId(p.id)} className="w-full text-left border border-[var(--leon-line)] rounded-lg p-3 bg-white hover:border-[var(--leon-brown)] hover:bg-[var(--leon-cream)] transition-colors">
                    <p className="font-bold text-sm">{p.name}</p>
                    <p className="text-xs text-[var(--leon-black)]/50">{p.address}</p>
                    <div className="mt-1.5"><Badge tone="neutral">{p.pipelineStatus}</Badge></div>
                  </button>
                ))}
              </div>
            )}
          </>
        )}
      </main>
      <AppFooter />
    </div>
  );
}
// The one nav bar in the app that had no icons. Same vocabulary as the staff
// side, so a client and a coordinator talking about "Shop Drawings" are looking
// at a tab that reads the same to both: 📐 is drawings everywhere in the Hub,
// 🎨 selections, 📄 the contract, 💵 money in.
const CLIENT_PROJECT_TABS = [
  { key: 'progress', label: 'Progress', icon: '📊' },
  { key: 'selections', label: 'Selections', icon: '🎨' },
  { key: 'drawings', label: 'Shop Drawings', icon: '📐' },
  { key: 'contract', label: 'Contract', icon: '📜' },
  { key: 'receivables', label: 'Receivables', icon: '💵' },
];
function ClientProjectDetail({ ctx, project, onBack }) {
  const [tab, setTab] = useState('progress');
  return (
    <div>
      <button onClick={onBack} className="text-sm text-[var(--leon-brown)] font-semibold mb-3">← Back to My Projects</button>
      <h1 className="text-xl font-bold">{project.name}</h1>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4">{project.address}</p>

      <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)] flex-wrap">
        {CLIENT_PROJECT_TABS.map(t => (
          <button key={t.key} onClick={() => setTab(t.key)} className={`subtab-btn shrink-0 whitespace-nowrap px-3 py-1.5 text-[12px] font-semibold border-b-2 ${tab === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>

      {tab === 'progress' && <ClientProgressTab project={project} />}
      {tab === 'selections' && <ClientSelectionsTab project={project} />}
      {tab === 'drawings' && <ClientShopDrawingsTab ctx={ctx} project={project} />}
      {tab === 'contract' && <ClientContractTab project={project} />}
      {tab === 'receivables' && <ClientReceivablesSection project={project} />}
    </div>
  );
}
function ClientProgressTab({ project }) {
  const subtabs = [
    ...(project.chronology && project.chronology.length > 0 ? [{ key: 'chronology', label: 'Lead / Take-off / Quotes', stages: project.chronology }] : []),
    ...project.scopes.map(scope => ({ key: scope.id, label: scope.name, stages: scope.stages })),
  ];
  const [sub, setSub] = useState(subtabs[0] ? subtabs[0].key : '');
  // Clients read progress two ways: as a checklist of what's done, or as dates
  // on a timeline. Neither is the "right" one, so both are offered.
  const [mode, setMode] = useState('list');
  const active = subtabs.find(s => s.key === sub) || subtabs[0];
  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-4 flex-wrap">
        <div className="flex gap-1 flex-wrap">
          {subtabs.map(s => (
            <button key={s.key} onClick={() => setSub(s.key)} className={`px-3 py-1.5 rounded-md text-xs font-semibold ${(sub || subtabs[0].key) === s.key ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 bg-[var(--leon-cream)] hover:bg-[var(--leon-line)]'}`}>{s.label}</button>
          ))}
        </div>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white shrink-0">
          {[{ k: 'list', label: 'List' }, { k: 'timeline', label: 'Timeline' }].map(v => (
            <button key={v.k} onClick={() => setMode(v.k)}
              className={`px-3 py-1 rounded-md text-xs font-semibold transition ${mode === v.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>
              {v.label}
            </button>
          ))}
        </div>
      </div>
      {!active ? <EmptyState text="No stages yet." />
        : mode === 'list' ? <ClientStageList stages={active.stages} />
        : <ClientStageTimeline stages={active.stages} label={active.label} />}
    </div>
  );
}

// Client-facing timeline. Deliberately simpler than the internal Gantt: no
// durations to misread, just where each stage sits in the run and whether it
// is done, moving, or still ahead — using the same schedule palette so the
// colours mean the same thing everywhere.
function ClientStageTimeline({ stages, label }) {
  const rows = (stages || []).filter(st => st.plannedStart || st.plannedDue);
  if (!rows.length) return <EmptyState text="No dated stages yet." />;
  const today = todayISO();
  const dates = rows.flatMap(st => [st.actualStart || st.plannedStart, st.actualCompletion || st.plannedDue]).filter(Boolean).sort();
  const min = dates[0], max = dates[dates.length - 1];
  const span = Math.max(1, daysBetween(min, max));
  const pct = d => Math.max(0, Math.min(100, (daysBetween(min, d) / span) * 100));
  const statusOf = st => st.status === 'Completed' ? 'Complete'
    : (st.plannedDue && st.plannedDue < today && st.status !== 'Completed') ? 'Delayed'
    : st.status === 'In Progress' ? 'In Progress' : 'Not Started';

  return (
    <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
      <div className="px-3 py-2 border-b border-[var(--leon-line)] flex items-center justify-between">
        <span className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">{label}</span>
        <span className="text-[11px] text-[var(--leon-black)]/40">{fmtDate(min)} &ndash; {fmtDate(max)}</span>
      </div>
      <div className="p-3 space-y-1.5">
        {rows.map(st => {
          const start = st.actualStart || st.plannedStart;
          const end = st.actualCompletion || st.plannedDue || start;
          const left = pct(start);
          const width = Math.max(2, pct(end) - left);
          const status = statusOf(st);
          return (
            <div key={st.id} className="flex items-center gap-2">
              <span className="w-40 shrink-0 text-[11px] text-[var(--leon-black)]/70 truncate" title={st.name}>{st.name}</span>
              <div className="relative flex-1 h-5 rounded bg-[var(--leon-cream)]">
                <div className="absolute rounded-full transition-transform hover:scale-y-125"
                     style={{ left: `${left}%`, width: `${width}%`, top: 4, height: 12, minWidth: 8,
                              background: scheduleGradient(status), boxShadow: '0 1px 3px rgba(22,19,17,.18)' }}
                     title={`${st.name}: ${fmtDate(start)} – ${fmtDate(end)} (${status})`} />
                {today >= min && today <= max && (
                  <div className="absolute top-0 bottom-0 w-px bg-[var(--leon-red)]" style={{ left: `${pct(today)}%` }} />
                )}
              </div>
              <span className="w-24 shrink-0 text-right text-[10px] text-[var(--leon-black)]/45">{fmtDate(end)}</span>
            </div>
          );
        })}
      </div>
      <div className="flex items-center gap-3 px-3 py-2 border-t border-[var(--leon-line)] text-[11px] text-[var(--leon-black)]/60 flex-wrap">
        {['Complete', 'In Progress', 'Delayed', 'Not Started'].map(k => (
          <span key={k} className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(k) }} /> {k}</span>
        ))}
        <span className="flex items-center gap-1.5"><span className="w-0.5 h-3 bg-[var(--leon-red)] inline-block" /> Today</span>
      </div>
    </div>
  );
}
// What the client sees of their own selections: the look they chose, per area.
// NOT who supplies it or under what code — a client picks a finish, and our
// sourcing is ours. clientSafeFinish (data.jsx) is the one filter, so there is
// a single place this decision lives rather than a rule remembered per screen.
function ClientSelectionsTab({ project }) {
  const scopes = (project.scopes || []).filter(sc => {
    const areas = [{ selections: sc.selections, supplierFinishes: sc.supplierFinishes }, ...(sc.selectionAreas || [])];
    return areas.some(a => Object.keys(a.supplierFinishes || {}).length || Object.values(a.selections || {}).some(Boolean));
  });
  if (!scopes.length) return <EmptyState text="No selections have been made yet. Your project team will share them here as they are confirmed." />;
  return (
    <div className="space-y-4">
      {scopes.map(sc => {
        const areas = [
          { id: 'main', name: sc.mainAreaName || 'Main Selections', selections: sc.selections, supplierFinishes: sc.supplierFinishes },
          ...(sc.selectionAreas || []),
        ];
        return (
          <div key={sc.id} className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
            <div className="px-3 py-2 bg-[var(--leon-cream)] flex items-center gap-2 flex-wrap">
              <span className="text-sm font-bold">{sc.name}</span>
              {sc.selectionsLocked && <Badge tone="green">Confirmed</Badge>}
            </div>
            {areas.map(a => {
              const finishes = Object.entries(a.supplierFinishes || {}).map(([catId, ref]) => ({ catId, f: clientSafeFinish(ref) })).filter(x => x.f);
              if (!finishes.length) return null;
              return (
                <div key={a.id} className="px-3 py-2 border-t border-[var(--leon-line)] first:border-0">
                  <p className="text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45 mb-2">{a.name}</p>
                  <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2">
                    {finishes.map(({ catId, f }) => (
                      <div key={catId} className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
                        {f.img ? <img src={f.img} alt="" className="w-full h-24 object-cover" /> : <div className="w-full h-24 bg-[var(--leon-cream)]" />}
                        <div className="px-2 py-1.5 leading-tight">
                          <div className="text-xs font-semibold truncate" title={f.name}>{f.name}</div>
                          {f.cat && <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{f.cat}</div>}
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              );
            })}
          </div>
        );
      })}
      <p className="text-[11px] text-[var(--leon-black)]/40">
        Ask your project contact if you would like a sample or a specification sheet for any of these.
      </p>
    </div>
  );
}

// The client's own view of the shop drawings, and where they answer them.
// A response here creates the SAME clientResponses thread a coordinator would
// log by hand (clientRespondToSubmittal) and moves the submittal's status, so
// the Shop Drawing Hub keeps one history rather than a portal one beside it.
function ClientShopDrawingsTab({ ctx, project }) {
  const shopDrawingScopes = project.scopes
    .map(scope => ({ scope, records: (scope.submittals || []).filter(t => !['Draft', 'Internal Review'].includes(t.status)) }))
    .filter(x => x.records.length > 0);
  const [sub, setSub] = useState(shopDrawingScopes[0] ? shopDrawingScopes[0].scope.id : '');
  const [respondTo, setRespondTo] = useState(null);
  if (shopDrawingScopes.length === 0) return <EmptyState text="No shop drawings on record yet." />;
  const active = shopDrawingScopes.find(x => x.scope.id === sub) || shopDrawingScopes[0];
  // Only a drawing actually sitting with the client is theirs to answer.
  const AWAITING = ['Submitted', 'Awaiting Response'];
  return (
    <div>
      <div className="flex gap-1 mb-4 flex-wrap">
        {shopDrawingScopes.map(x => {
          const waiting = x.records.filter(t => AWAITING.includes(t.status)).length;
          return (
            <button key={x.scope.id} onClick={() => setSub(x.scope.id)} className={`px-3 py-1.5 rounded-md text-xs font-semibold ${(sub || shopDrawingScopes[0].scope.id) === x.scope.id ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 bg-[var(--leon-cream)] hover:bg-[var(--leon-line)]'}`}>
              {x.scope.name} ({x.records.length}){waiting > 0 && <span className="ml-1.5 text-[var(--leon-yellow)]">&#9679; {waiting}</span>}
            </button>
          );
        })}
      </div>
      <div className="space-y-1.5">
        {active.records.map(t => {
          const latest = t.revisions[t.revisions.length - 1];
          const awaiting = AWAITING.includes(t.status);
          const answered = (active.scope.clientResponses || []).filter(r => r.respondingToSubmittalId === t.id);
          const mine = answered[answered.length - 1];
          return (
            <div key={t.id} className="border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs">
              <div className="flex items-center justify-between gap-2 flex-wrap">
                <div className="min-w-0">
                  <p className="font-semibold truncate">{t.name}</p>
                  <p className="text-[var(--leon-black)]/50">Rev. {latest.revisionNumber} &middot; {fmtDate(latest.date)}</p>
                </div>
                <div className="flex items-center gap-2 shrink-0">
                  {awaiting && <Badge tone="yellow">Needs your response</Badge>}
                  <StatusBadge status={t.status} />
                  <AttachmentLink name={latest.file || t.name} url={latest.fileUrl} />
                  {awaiting && <Button size="sm" onClick={() => setRespondTo({ scope: active.scope, thread: t, rev: latest })}>Respond</Button>}
                </div>
              </div>
              {mine && (
                <p className="mt-1.5 pt-1.5 border-t border-[var(--leon-line)] text-[var(--leon-black)]/55">
                  <b>{mine.status}</b> &mdash; {fmtDate(mine.revisions[0].date)} by {mine.revisions[0].responsiblePerson}
                  {mine.revisions[0].notes ? ` — “${mine.revisions[0].notes}”` : ''}
                </p>
              )}
            </div>
          );
        })}
      </div>
      <ClientRespondModal ctx={ctx} project={project} target={respondTo} onClose={() => setRespondTo(null)} />
    </div>
  );
}

// The three answers a client actually gives. They are the workflow's own
// statuses, not a separate portal vocabulary — an "Approved as Noted" from the
// client means the same thing internally as one entered by a coordinator.
const CLIENT_SUBMITTAL_OUTCOMES = [
  { key: 'Approved', label: 'Approve', hint: 'Build it as drawn.' },
  { key: 'Approved as Noted', label: 'Approve with notes', hint: 'Proceed, with the changes described below.' },
  { key: 'Revise & Resubmit', label: 'Revise & resubmit', hint: 'Not yet — send a new revision.' },
];
function ClientRespondModal({ ctx, project, target, onClose }) {
  const [outcome, setOutcome] = useState('');
  const [notes, setNotes] = useState('');
  const [file, setFile] = useState({ name: null, url: null });
  useEffect(() => { if (target) { setOutcome(''); setNotes(''); setFile({ name: null, url: null }); } }, [target && target.thread.id]);
  if (!target) return null;
  const chosen = CLIENT_SUBMITTAL_OUTCOMES.find(o => o.key === outcome);
  // Anything other than a clean approval needs a reason — a bare "revise" gives
  // the shop nothing to act on and just costs another round trip.
  const needsNote = outcome && outcome !== 'Approved';
  const ready = !!outcome && (!needsNote || notes.trim());
  function submit() {
    ctx.clientRespondToSubmittal(project.id, target.scope.id, target.thread.id,
      { status: outcome, notes: notes.trim(), file: file.name, fileUrl: file.url });
    onClose();
  }
  return (
    <Modal open={!!target} onClose={onClose} title={`Respond — ${target.thread.name}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!ready}>{ready ? `Send ${chosen.label.toLowerCase()}` : needsNote ? 'Add a note first' : 'Choose a response'}</Button></>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/55">
          {target.scope.name} &middot; Rev. {target.rev.revisionNumber} of {fmtDate(target.rev.date)}
        </p>
        <div className="space-y-1.5">
          {CLIENT_SUBMITTAL_OUTCOMES.map(o => (
            <button key={o.key} type="button" onClick={() => setOutcome(o.key)}
              className={`w-full text-left border rounded-lg px-3 py-2 ${outcome === o.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown-light)]'}`}>
              <span className="text-sm font-semibold">{o.label}</span>
              <span className="block text-xs text-[var(--leon-black)]/50">{o.hint}</span>
            </button>
          ))}
        </div>
        <Field label={needsNote ? 'What needs to change?' : 'Notes'} hint={needsNote ? 'Required — the shop needs to know what to do differently.' : 'Optional'}>
          <TextArea rows={3} value={notes} onChange={e => setNotes(e.target.value)}
            placeholder="e.g. move the appliance garage to the left of the sink run" />
        </Field>
        <Field label="Marked-up drawing (optional)">
          <FileField name={file.name} url={file.url} editable placeholder="Upload a marked-up PDF"
            onChange={(name, url) => setFile({ name, url })} />
        </Field>
        <p className="text-[11px] text-[var(--leon-black)]/45">
          Your response is recorded against this drawing and the project team is notified straight away.
        </p>
      </div>
    </Modal>
  );
}

function ClientContractTab({ project }) {
  return (
    <div>
      <div className="grid sm:grid-cols-3 gap-3 mb-3">
        <StatBox label="Original Contract" value={fmtMoney(project.originalContractValue || 0)} />
        <StatBox label="Approved Change Orders" value={fmtMoney(approvedChangeOrderTotal(project))} />
        <StatBox label="Revised Contract Value" value={fmtMoney(revisedContractValue(project))} />
      </div>
      {(project.changeOrders || []).length === 0 ? <EmptyState text="No change orders on record." /> : (
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-2">#</th><th className="py-1 pr-2">Description</th><th className="py-1 pr-2">Date</th><th className="py-1 pr-2">Amount</th><th className="py-1">Status</th></tr></thead>
            <tbody>
              {project.changeOrders.map(co => (
                <tr key={co.id} className="border-t border-[var(--leon-line)]">
                  <td className="py-1.5 pr-2 whitespace-nowrap">{co.number}</td>
                  <td className="py-1.5 pr-2">{co.description}</td>
                  <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(co.date)}</td>
                  <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{fmtMoney(co.amount)}</td>
                  <td className="py-1.5"><StatusBadge status={co.status} /></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}
// Deliberately NOT a reuse of AccountsReceivableSubTab: that component also
// renders a "Profitability" section (estimated/actual cost, margins) with no
// editable-only gate at all, since it was built assuming an internal
// audience — exactly the kind of internal cost data a client must never see.
// This is a from-scratch, client-safe subset: Payment Terms, Retainage, and
// Payment Requisitions only, using the same lib.jsx helpers.
function ClientReceivablesSection({ project }) {
  const terms = paymentTermsWithAmounts(project);
  const rcv = revisedContractValue(project);
  const totalRetainage = totalRetainageHeld(project);
  return (
    <>
      <Collapsible title="Payment Terms" count={terms.length}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">Calculated against the revised contract value of {fmtMoney(rcv)}.</p>
        <div className="space-y-1.5">
          {terms.map(t => (
            <div key={t.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
              <div>
                <p className="text-sm font-semibold">{t.label} <span className="text-[var(--leon-black)]/40 text-xs font-normal">{t.pct}% · {t.trigger}</span></p>
                <p className="text-xs text-[var(--leon-black)]/50">{fmtMoney(t.amount)}</p>
              </div>
              <StatusBadge status={t.status} />
            </div>
          ))}
        </div>
      </Collapsible>
      <Collapsible title="Retainage">
        <StatBox label="Total Retainage Held to Date" value={fmtMoney(totalRetainage)} />
      </Collapsible>
      {project.paymentRequisitions.length > 0 && (
        <Collapsible title="Payment Requisitions" count={project.paymentRequisitions.length}>
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-2">Rev</th><th className="py-1 pr-2">Type</th><th className="py-1 pr-2">Amount</th><th className="py-1 pr-2">Date</th><th className="py-1">Status</th></tr></thead>
              <tbody>
                {project.paymentRequisitions.map(r => (
                  <tr key={r.id} className="border-t border-[var(--leon-line)]">
                    <td className="py-1.5 pr-2 whitespace-nowrap">R{r.revision}</td>
                    <td className="py-1.5 pr-2">{r.type}</td>
                    <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{fmtMoney(r.amount)}</td>
                    <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(r.date)}</td>
                    <td className="py-1.5"><StatusBadge status={r.status} /></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </Collapsible>
      )}
    </>
  );
}
function ClientStageList({ stages }) {
  if (!stages || stages.length === 0) return <EmptyState text="No stages yet." />;
  return (
    <div className="space-y-1">
      {stages.map(st => (
        <div key={st.id} className="flex items-center justify-between gap-2 border-b border-[var(--leon-line)] last:border-0 py-1.5 text-xs">
          <span>{st.name}</span>
          <div className="flex items-center gap-2 shrink-0">
            {st.plannedDue && <span className="text-[var(--leon-black)]/40">Due {fmtDate(st.plannedDue)}</span>}
            <StatusBadge status={st.status} />
          </div>
        </div>
      ))}
    </div>
  );
}
// "View As" dropdown grouping (Header, below) — Admin/Accounting pick who to
// preview the app as, grouped by login type rather than one flat
// alphabetical list, since Subcontractor/Driver/Client logins are a very
// different kind of thing to preview than an internal Employee.
const VIEW_AS_GROUPS = [
  { label: 'Employees', match: p => !['Subcontractor', 'Delivery Driver', 'Client'].includes(p.securityRole) },
  { label: 'Subcontractors', match: p => p.securityRole === 'Subcontractor' },
  { label: 'Drivers', match: p => p.securityRole === 'Delivery Driver' },
  { label: 'Clients', match: p => p.securityRole === 'Client' },
];
function viewAsPersonLabel(p, ctx) {
  if (p.securityRole === 'Subcontractor') {
    const sub = ctx.subcontractors.find(s => s.id === p.subcontractorId);
    return sub ? `${p.name} — ${sub.companyName}` : p.name;
  }
  if (p.securityRole === 'Client') {
    const acct = ctx.accounts.find(a => a.id === p.accountId);
    return acct ? `${p.name} — ${acct.name}` : p.name;
  }
  return p.name;
}
// The five offices, and what time it is in each. Ticks once a minute — a second
// hand would re-render the whole header sixty times a minute to show something
// nobody reads. The viewer's own office is marked, and a city on a different
// calendar day carries +1 / −1, which is the thing that actually catches people
// out when they book a call.
function WorldClock({ ctx }) {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const tick = () => setNow(new Date());
    // Line up with the top of the minute, then run every minute, so the digits
    // change when the clock does rather than up to 59 seconds late.
    const ms = 60000 - (Date.now() % 60000);
    let id2;
    const id1 = setTimeout(() => { tick(); id2 = setInterval(tick, 60000); }, ms);
    return () => { clearTimeout(id1); if (id2) clearInterval(id2); };
  }, []);
  const mine = officeByKey(ctx.currentUser && ctx.currentUser.officeLocation);
  return (
    <div className="flex flex-wrap items-center justify-end gap-x-1 gap-y-0.5 text-[11px] leading-none">
      {LEON_OFFICES.map((o, i) => {
        const isMine = mine && mine.key === o.key;
        const off = dayOffsetInZone(o.tz, now);
        return (
          <React.Fragment key={o.key}>
            {i > 0 && <span aria-hidden="true" className="text-[var(--leon-black)]/20 px-1">|</span>}
            <span title={`${o.city}, ${o.country} — ${o.tz}`}
              className={`inline-flex items-baseline gap-1 whitespace-nowrap ${isMine ? 'text-[var(--leon-brown)] font-semibold' : 'text-[var(--leon-black)]/55'}`}>
              <span aria-hidden="true">{o.flag}</span>
              <span>{o.city}</span>
              <span className="tabular-nums font-semibold">{timeInZone(o.tz, now)}</span>
              {off !== 0 && <span className="opacity-60">{off > 0 ? '+1' : '\u22121'}</span>}
            </span>
          </React.Fragment>
        );
      })}
    </div>
  );
}

function Header({ ctx, view }) {
  const [showProfile, setShowProfile] = useState(false);
  const [openMenu, setOpenMenu] = useState(null);
  // Fourteen flat entries had outgrown one row and left the nav scrolling
  // sideways. The daily destinations stay one click away; the ones you visit
  // occasionally are gathered under two menus, which keeps the bar to a single
  // line at any normal window width.
  // LEON Library used to build its own item list here and hand it to a bare
  // NavMenu. That is what left it as the one hub whose NAME did nothing — it
  // opened a list and went nowhere. It goes through `hubMenu` now, like every
  // other hub, which lists the sections AND lands on one.
  // LEON Collection and Users are both places you go to CONFIGURE the Hub
  // rather than to do a job in it, so they sit together under Admin Settings
  // instead of one being filed with the catalogs and the other floating in the
  // main bar. The Collection's own sections are listed rather than the hub, so
  // Lead Times or Holidays is one click from the bar like everything else.
  const adminItems = [
    ...(ctx.canManageCollection ? LEON_COLLECTION_TABS.map(t => ({
      key: t.key, label: t.label, icon: t.icon,
      go: () => { ctx.setHubSection('admin', t.key); ctx.goAdmin(); },
    })) : []),
    ...(ctx.canView('users') ? [{ key: 'users', label: 'Users', icon: '🔑', go: ctx.goUsers }] : []),
  ];
  // LEON Studio's "sections" are the tools themselves. The blank key is the
  // tile grid you land on, which is a real destination and not a placeholder.
  const studioItems = [
    { key: '', label: 'All tools', icon: '🏛️' },
    ...LEON_SOFTWARE_LINKS.filter(l => l.key).map(l => ({ key: l.key, label: l.name, icon: l.icon })),
  ];
  // One nav entry that both goes somewhere and lists what is inside it.
  // `views` is every view key that counts as "you are in this hub", so a legacy
  // key such as `warehouse` or `map` still highlights the right button.
  const hubMenu = (hub, views, label, icon, tabs, go, fallback) => (
    <NavMenu key={hub} label={label} icon={icon} keepLabel
      items={tabs.map(t => ({
        key: t.key, label: t.label || t.name, icon: t.icon,
        go: () => { ctx.setHubSection(hub, t.key); go(); },
      }))}
      view={views.indexOf(view) >= 0 ? (ctx.hubSections[hub] !== undefined ? ctx.hubSections[hub] : fallback) : null}
      onTrigger={go}
      open={openMenu === hub} onToggle={() => setOpenMenu(openMenu === hub ? null : hub)} />
  );


  return (
    <header className="no-print border-b border-[var(--leon-line)] bg-white sticky top-0 z-30" onClick={() => setOpenMenu(null)}>
      {/* A thin status strip for the five offices. The clock is ambient — read
          at a glance, never clicked — so it does not belong among the controls,
          where it was 436px wide and forced the whole utility strip onto two
          ragged lines. On its own strip it has room to read as one line per
          city, and the row below gets that width back. Same container as
          everything else, so it ends on the page's own right edge. */}
      <div className="border-b border-[var(--leon-line)] bg-[var(--leon-cream)]/50">
        <div className="max-w-7xl mx-auto px-4 md:px-6 py-1">
          <WorldClock ctx={ctx} />
        </div>
      </div>
      {/* Everything in the header sits on the SAME grid as the page below it:
          one container, one left edge, one right edge. The logo starts where
          every page heading starts, the nav row runs the full width from that
          same edge, and the utility strip ends where the page content ends.
          Nothing is nested inside a narrower column, which is what used to make
          the nav look indented against the rest of the app. */}
      <div className="max-w-7xl mx-auto px-4 md:px-6 py-2 flex flex-col gap-1.5">

        {/* Identity + utilities. Wraps rather than hiding: the department
            filter and View As were behind `lg:` and simply vanished on a
            laptop-width window, which reads as them not existing. */}
        <div className="flex items-center gap-3 md:gap-5">
          <button onClick={ctx.goDashboard} className="flex items-center gap-3 shrink-0">
            <img src="logo/leon-mark.svg" alt="LEON" className="h-20 md:h-28 w-auto rounded-sm" />
            {/* LEON is the real vector wordmark from the brand file, not type set
                in a substitute font — the E is three bars with a metallic gradient
                on the top one, which no font can reproduce. The subtitle is set to
                the same width, the way LEON sits over COLLECTION in the brand
                lockup. Do not replace this with text. */}
            <span className="hidden sm:flex flex-col items-center">
              <img src="logo/leon-wordmark.svg" alt="LEON" className="block h-7 lg:h-9 w-auto" />
              <span className="block text-[9px] tracking-[0.2em] lg:tracking-[0.38em] text-[var(--leon-brown)] leading-none mt-1.5 pl-[0.2em] lg:pl-[0.38em]">OPERATIONS HUB</span>
            </span>
          </button>

          <div className="flex-1" />

          {/* One right-aligned group that wraps INSIDE itself. Wrapping the
              outer row instead left Log Out stranded at the left edge of a
              second line; justify-end keeps a wrapped row hard against the
              same right edge as the content below. */}
          <div className="flex items-center justify-end gap-2 md:gap-3 flex-wrap">
          {/* Search, department and View As are one set of controls: one width,
              one row, and they wrap together rather than splitting apart. */}
          <div className="flex items-center gap-2 md:gap-2.5">
          <div className="w-36 sm:w-40 lg:w-44"><GlobalSearch ctx={ctx} /></div>
          {/* Department scope selector — the Windows/Interiors filter this
              person is currently looking through. Only a person who covers
              BOTH departments has a choice to make; for everyone else this
              collapses to a static label so the header still says plainly
              which department's data they're seeing. */}
          {ctx.canChooseDepartment ? (
            <Select
              value={ctx.departmentChoice}
              onChange={e => ctx.setDepartmentChoice(e.target.value)}
              className={`!w-36 lg:!w-44 !py-1 !text-xs ${ctx.activeDepartment !== ALL_DEPARTMENTS ? '!border-[var(--leon-brown)] !text-[var(--leon-brown)] font-semibold' : ''}`}
              title="Filter the whole app to one department"
            >
              <option value={ALL_DEPARTMENTS}>All Departments</option>
              {ctx.myDepartments.map(d => <option key={d} value={d}>{d} only</option>)}
            </Select>
          ) : (
            <span className="px-2 py-1 rounded border border-[var(--leon-brown)]/30 bg-[var(--leon-brown)]/5 text-[10px] uppercase tracking-wide font-semibold text-[var(--leon-brown)] whitespace-nowrap" title="Your account covers this department only">
              {ctx.activeDepartment} Dept.
            </span>
          )}
          {ctx.canViewAsOthers && (
            <Select
              value={ctx.isViewingAs ? ctx.viewAsUserId : ''}
              onChange={e => ctx.setViewAsUserId(e.target.value || null)}
              className={`!w-36 lg:!w-44 !py-1 !text-xs ${ctx.isViewingAs ? '!border-[var(--leon-red)] !text-[var(--leon-red)]' : ''}`}
              title="View the app as another user"
            >
              <option value="">View As: Me</option>
              {VIEW_AS_GROUPS.map(g => {
                const people = ctx.teamDirectory.filter(p => p.active && p.id !== ctx.realCurrentUser.id && g.match(p));
                if (people.length === 0) return null;
                return (
                  <optgroup key={g.label} label={g.label}>
                    {people.map(p => <option key={p.id} value={p.id}>{viewAsPersonLabel(p, ctx)}</option>)}
                  </optgroup>
                );
              })}
            </Select>
          )}
          </div>
          {/* Bell, identity and Log Out travel together — wrapping them apart
              left Log Out stranded on a line of its own. */}
          <div className="flex items-center gap-2 md:gap-4">
          <NotificationBell ctx={ctx} />
          <button onClick={() => setShowProfile(true)} className="flex items-center gap-2 hover:opacity-75"
            title={`${ctx.currentUserName}${personTitle(ctx.currentUser) ? ' \u2014 ' + personTitle(ctx.currentUser) : ''} \u2014 edit my profile`}>
            {/* Name only. The job title is on the profile, and here it ran to
                half the row — "Carolline Martire — Director of Operations &
                Interior Finishes" — which is what pushed the controls onto a
                second line. The title still shows on hover. */}
            <div className="hidden md:flex flex-col items-end leading-tight">
              <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Signed in as</span>
              <span className="text-xs font-semibold whitespace-nowrap">{ctx.currentUserName}</span>
            </div>
            <Avatar name={ctx.currentUserName} url={ctx.currentUser.photoUrl} size={32} />
          </button>
          <button onClick={ctx.logout} className="no-print text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">Log Out</button>
          </div>
          </div>
        </div>

        {/* Navigation — full width, starting on the page's own left edge. */}
        <nav className="flex items-center gap-0.5 flex-wrap border-t border-[var(--leon-line)] pt-1.5">
          {/* The client's own running order. Every destination that HAS subtabs
              lists them here: clicking the button still opens the hub in one
              click (onTrigger) and drops the section list at the same time, so
              nothing got slower by gaining a menu, and the open section is
              ticked inside. Dashboard and Reports stay plain buttons — the
              first is a block layout and the second is a searchable list of 69
              reports, neither of which has subtabs to list.
              Inventory is not its own destination — stock is something
              Logistics holds, so it is a subtab in there. `goWarehouse` is KEPT
              and redirects, because the last view is restored from
              sessionStorage and an old session must still land somewhere. */}
          <NavBtn active={view === 'dashboard'} onClick={ctx.goDashboard} icon="🏠">Dashboard</NavBtn>
          {hubMenu('calendar', ['calendar'], 'Calendar', '📅', CALENDAR_HUB_SUBTABS, ctx.goCalendar, 'myToDo')}
          {hubMenu('accounts', ['accounts', 'map'], 'Accounts', '🤝', ACCOUNTS_LIST_SUBTABS, ctx.goAccounts, 'accounts')}
          {/* The Vendors module row existed in the matrix and was consulted
              NOWHERE — the nav and the screen were both open to anyone who
              reached the app shell. That included the Installation Team, whose
              allowlist says in as many words that Installation is the full
              extent of what they should see. */}
          {ctx.canView('vendors') && hubMenu('vendors', ['vendors'], 'Vendors', '🏭', VENDOR_DIRECTORY_TABS, ctx.goVendors, 'vendor')}
          {hubMenu('sales', ['sales'], 'Sales', '💼', SALES_PIPELINE_SUBTABS, ctx.goSales, 'overview')}
          {ctx.canSeeAccountingHub &&
            hubMenu('accounting', ['accounting'], 'Accounting', '💰', ACCOUNTING_TABS, ctx.goAccounting, 'calendar')}
          {/* Same permission split the hub itself applies: a warehouse assistant
              may hold stock rights without logistics rights, so the menu offers
              exactly the subtabs that person can open. */}
          {(ctx.canSeeLogistics || ctx.canAllocateMaterial || ctx.canAssistWarehouse || ctx.canCreateInventory) &&
            hubMenu('logistics', ['logistics', 'warehouse'], 'Logistics', '🚚',
              LOGISTICS_HUB_SUBTABS.filter(t => t.key === 'inventory'
                ? (ctx.canAllocateMaterial || ctx.canAssistWarehouse || ctx.canCreateInventory)
                : ctx.canSeeLogistics),
              ctx.goLogistics, 'dashboard')}
          {/* Clicking the name lands on the library AND drops the section list,
              the same one-click behaviour Accounts and Calendar have. It opens
              whichever section you last had open, and Documents the first time.
              `training` is in the view list because HUB Training is a section
              of this hub, so a restored nav state still highlights it. */}
          {hubMenu('leonLibrary', ['leonLibrary', 'training'], 'LEON Library', '📚',
                   LEON_LIBRARY_SUBTABS, ctx.goLeonLibrary, 'documents')}
          {hubMenu('leonStudio', ['leonStudio', 'softwares', 'office'], 'LEON Studio', '🏛️', studioItems, ctx.goLeonStudio, '')}
          {hubMenu('aboutUs', ['aboutUs'], 'About Us', '🏢', ABOUT_US_SUBTABS, ctx.goAboutUs, 'company')}
          <NavBtn active={view === 'reports'} onClick={ctx.goReports} icon="📊">Reports</NavBtn>
          {adminItems.length > 0 && (
            /* Unlike the other menus this one spans TWO views — the Collection's
               sections and Users — so it cannot go through hubMenu, which calls
               a single `go`. It lands wherever the person is actually allowed to
               be: the Collection when they can manage it, otherwise Users, which
               is the only other thing in here. */
            <NavMenu label="Admin Settings" icon="⚙️" items={adminItems} keepLabel
                     view={view === 'admin' ? (ctx.hubSections.admin || 'scopes') : (view === 'users' ? 'users' : null)}
                     onTrigger={ctx.canManageCollection ? ctx.goAdmin : ctx.goUsers}
                     open={openMenu === 'admin'} onToggle={() => setOpenMenu(openMenu === 'admin' ? null : 'admin')} />
          )}
          {/* The search moves inline on narrow screens, where the row above hides it. */}
          <div className="md:hidden w-full pt-1"><GlobalSearch ctx={ctx} /></div>
        </nav>
      </div>

      {ctx.isViewingAs && (
        <div className="bg-[var(--leon-red)] text-white text-center text-xs font-semibold py-1.5 flex items-center justify-center gap-3">
          <span>&#9888; Viewing as {ctx.currentUserName} ({ctx.currentRole}) &mdash; actions you take now are recorded as {ctx.currentUserName}, not {ctx.realCurrentUser.name}.</span>
          <button onClick={() => ctx.setViewAsUserId(null)} className="underline font-bold">Return to my account</button>
        </div>
      )}
      <MyProfileModal open={showProfile} onClose={() => setShowProfile(false)} ctx={ctx} />
    </header>
  );
}
// The portals (Subcontractor / Client / Delivery Driver) get the same inbox the
// staff app has, in a compact form: what was shared or assigned to this person,
// newest first. Portal users have no notification-preferences screen, so a
// share always reaches them here — email is the optional extra, not the other
// way round.
function PortalInbox({ ctx }) {
  const [open, setOpen] = useState(false);
  const mine = (ctx.notifications || []).filter(n => n.toUserId === ctx.currentUserId);
  const unread = mine.filter(n => !n.read).length;
  return (
    <div className="relative shrink-0" onClick={e => e.stopPropagation()}>
      <button onClick={() => setOpen(o => !o)} title="Messages"
        className={`relative w-9 h-9 rounded-full grid place-items-center text-lg transition ${open ? 'bg-[var(--leon-cream)]' : 'hover:bg-[var(--leon-cream)]'}`}>
        <span aria-hidden="true">&#128276;</span>
        {unread > 0 && (
          <span className="absolute -top-0.5 -right-0.5 min-w-[17px] h-[17px] px-1 rounded-full bg-[var(--leon-red)] text-white text-[10px] font-bold grid place-items-center">
            {unread > 9 ? '9+' : unread}
          </span>
        )}
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-1 z-40 w-80 max-w-[92vw] bg-white border border-[var(--leon-line)] rounded-xl shadow-xl overflow-hidden">
          <div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--leon-line)]">
            <p className="text-sm font-bold">Messages</p>
            <div className="flex-1" />
            {unread > 0 && (
              <button onClick={() => ctx.markAllNotificationsRead()} className="text-[11px] font-semibold text-[var(--leon-brown)] hover:underline">Mark all read</button>
            )}
          </div>
          <div className="max-h-96 overflow-y-auto">
            {mine.length === 0 ? (
              <p className="px-3 py-6 text-center text-sm text-[var(--leon-black)]/40">Nothing yet.</p>
            ) : mine.slice(0, 20).map(n => (
              <div key={n.id} className={`px-3 py-2 border-b border-[var(--leon-line)] last:border-0 ${n.read ? '' : 'bg-[var(--leon-cream)]/60'}`}>
                <div className="flex items-start gap-2">
                  {!n.read && <span className="w-1.5 h-1.5 rounded-full bg-[var(--leon-brown)] mt-2 shrink-0" />}
                  <div className="min-w-0 flex-1">
                    <p className="text-sm font-semibold leading-snug">{n.title}</p>
                    {n.body && <p className="text-[11px] text-[var(--leon-black)]/55 leading-snug whitespace-pre-wrap">{n.body}</p>}
                    <p className="text-[10px] text-[var(--leon-black)]/35 mt-0.5">
                      {[n.byUser, n.projectName, fmtDate(n.date)].filter(Boolean).join(' \u00b7 ')}
                    </p>
                  </div>
                  <button onClick={() => ctx.markNotificationRead(n.id, !n.read)}
                    className="text-[10px] text-[var(--leon-black)]/35 hover:underline shrink-0">{n.read ? 'unread' : 'read'}</button>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================================
// Share — "who should see this?", and nothing more
// ============================================================================
// Drop <ShareButton subject=… summary=… link=… projectId=… /> next to anything
// worth sending someone. The modal answers one question at a time: who, then
// what to say. Colleagues get it in their inbox now; anyone with an email
// address gets a message queued for the mail service.
// `beforeOpen` is an optional gate: return false and the modal does not open,
// which lets a caller ask something first — a quotation asks whether to issue
// the revision before it leaves the building. Undefined keeps every existing
// call site behaving exactly as it did.
// `openSignal` is the other half of `beforeOpen`: a caller that held the modal
// shut to ask something first increments this to open it once the answer is in,
// so the person presses Share once rather than twice.
function ShareButton({ ctx, subject, summary, link, projectId, subjectKey, items, size, variant, label, beforeOpen, openSignal }) {
  const [open, setOpen] = useState(false);
  const [showLog, setShowLog] = useState(false);
  const log = (ctx.shares || []).filter(x => x.subjectKey === subjectKey);
  const ask = () => { if (beforeOpen && beforeOpen() === false) return; setOpen(true); };
  // Only ever opens; a signal of 0 (the initial value) never fires, so a mount
  // cannot pop the modal open on its own.
  useEffect(() => { if (openSignal) setOpen(true); }, [openSignal]);
  return (
    <>
      {/* An icon, like Print and PDF beside it. The modal already asks what to
          include, so the button never needed to say "package". */}
      {label
        ? <Button size={size || 'sm'} variant={variant || 'outline'} onClick={ask}>{label}</Button>
        : <IconAction icon="↪" onClick={ask}
            title={items && items.length ? `Share ${subject} — choose what to include` : `Share ${subject}`} />}
      {/* The count sits next to the button so "has this gone out already?" is
          answered before you send it a second time. */}
      {log.length > 0 && (
        <button onClick={() => setShowLog(true)}
          className="ml-1.5 text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap"
          title={`Shared ${log.length} time${log.length === 1 ? '' : 's'}`}>
          shared {log.length}&times;
        </button>
      )}
      <ShareModal open={open} onClose={() => setOpen(false)} ctx={ctx}
        subject={subject} summary={summary} link={link} projectId={projectId} subjectKey={subjectKey} items={items} />
      <Modal wide open={showLog} onClose={() => setShowLog(false)} title={`Share history — ${subject}`}
        footer={<Button variant="ghost" onClick={() => setShowLog(false)}>Close</Button>}>
        <ShareLog ctx={ctx} subjectKey={subjectKey} />
      </Modal>
    </>
  );
}

// Who sent it, to whom, when, and what they said. Filter by `subjectKey` for
// one item's trail, by `projectId` for everything shared off a job, or neither
// for the whole company.
function ShareLog({ ctx, subjectKey, projectId, limit }) {
  let rows = ctx.shares || [];
  if (subjectKey) rows = rows.filter(r => r.subjectKey === subjectKey);
  if (projectId) rows = rows.filter(r => r.projectId === projectId);
  if (limit) rows = rows.slice(0, limit);
  if (!rows.length) return <EmptyState text="Not shared with anyone yet." />;
  return (
    <div className="space-y-1.5">
      {rows.map(r => (
        <div key={r.id} className="border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-white">
          <div className="flex items-baseline gap-2 flex-wrap">
            <p className="text-sm font-semibold">{r.subject}</p>
            <span className="text-[11px] text-[var(--leon-black)]/45">
              by <b>{r.by}</b> on {fmtDate(r.date)}
              {r.stamp ? ` at ${new Date(r.stamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}` : ''}
            </span>
          </div>
          <div className="flex flex-wrap gap-1 mt-1">
            {r.recipients.map((p, i) => (
              <span key={i} className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold border ${p.channel === 'inbox' ? 'border-[var(--leon-green)]/50 text-[var(--leon-green)]' : 'border-[var(--leon-yellow)]/60 text-[var(--leon-yellow)]'}`}
                    title={p.channel === 'inbox' ? 'Delivered to their inbox in the app' : `Queued to ${p.email}`}>
                {p.name}
                <span className="font-normal opacity-70">{p.channel === 'inbox' ? 'inbox' : 'email'}</span>
              </span>
            ))}
          </div>
          {(r.items || []).length > 0 && (
            <p className="text-[11px] text-[var(--leon-black)]/55 mt-1">
              <b>{r.items.length}{r.itemsTotal ? ` of ${r.itemsTotal}` : ''}</b> item{r.items.length === 1 ? '' : 's'}: {r.items.map(i => i.label).join(', ')}
            </p>
          )}
          {r.message && <p className="text-xs text-[var(--leon-black)]/60 mt-1 italic">&ldquo;{r.message}&rdquo;</p>}
          {r.projectName && !projectId && <p className="text-[10px] text-[var(--leon-black)]/35 mt-0.5">{r.projectName}</p>}
        </div>
      ))}
    </div>
  );
}

function ShareModal({ open, onClose, ctx, subject, summary, link, projectId, subjectKey, items }) {
  // Two audiences, two behaviours, so they are two tabs rather than one mixed
  // list. INTERNAL lands in a colleague's in-app inbox and also emails them;
  // EXTERNAL has no inbox here, so email is the only channel — and saying that
  // plainly on the tab stops anyone assuming a client saw it in the app.
  const [tab, setTab] = useState('internal');
  const [picked, setPicked] = useState([]);      // [{ userId?, email?, name, scope }]
  const [q, setQ] = useState('');
  const [extra, setExtra] = useState('');
  const [message, setMessage] = useState('');
  // The branded version is what actually goes out; the plain-text half is the
  // fallback a client without HTML gets, and is worth being able to check.
  const [previewText, setPreviewText] = useState(false);
  const [sent, setSent] = useState(null);
  // A layered record shares as a PACKAGE. Everything is ticked to start —
  // "send them the lot" is the common case — and unticking is how you narrow it.
  const [chosen, setChosen] = useState(() => new Set());
  useEffect(() => {
    if (!open) return;
    setTab('internal'); setPicked([]); setQ(''); setExtra(''); setMessage(''); setSent(null);
    setChosen(new Set((items || []).map(i => i.id)));
  }, [open, items]);
  const pack = items || [];
  const chosenItems = pack.filter(i => chosen.has(i.id));
  // Preserve the order the caller supplied; '' is the ungrouped bucket.
  const packGroups = [...new Set(pack.map(i => i.group || ''))];

  const project = projectId ? ctx.projects.find(p => p.id === projectId) : null;
  const account = project ? ctx.accounts.find(a => a.id === project.accountId) : null;

  // "Our own people" means STAFF. Client, Subcontractor and Delivery Driver
  // logins live in teamDirectory too — they are portal accounts, not
  // colleagues — so they are excluded here and appear under Externally, which
  // is where someone looks for them.
  const internalOptions = ctx.teamDirectory
    .filter(p => p.active && p.id !== ctx.currentUserId && !PORTAL_ROLES.includes(p.securityRole))
    .map(p => ({ userId: p.id, name: p.name, email: p.email, sub: p.title || p.securityRole, scope: 'internal' }));

  // Everyone outside LEON this job already knows about — the client's contacts,
  // its vendors and its subcontractors — so the common case is one click.
  // An external party may still have a PORTAL LOGIN here — clients and
  // subcontractors do. When they do, the share should land in their portal
  // inbox as well as their email, so `portalLoginFor` looks one up and the
  // option carries a userId. Without this a client would be emailed about
  // something they could have simply seen when they next signed in.
  const portalLoginFor = email => {
    if (!email) return null;
    const e = email.toLowerCase();
    return ctx.teamDirectory.find(p => p.active && (p.email || '').toLowerCase() === e
      && PORTAL_ROLES.includes(p.securityRole)) || null;
  };
  const withPortal = o => {
    const login = portalLoginFor(o.email);
    return login ? { ...o, userId: login.id, portal: true } : o;
  };
  const externalOptions = [
    ...(account && account.contactName && account.email
      ? [{ email: account.email, name: account.contactName, sub: `${account.name} \u00b7 ${account.title || 'Client contact'}`, scope: 'external' }] : []),
    ...(account ? (account.contacts || []).filter(c => c.email).map(c =>
      ({ email: c.email, name: c.name, sub: `${account.name} \u00b7 ${c.role || 'Client contact'}`, scope: 'external' })) : []),
    ...(ctx.vendors || []).filter(v => v.active !== false && v.email).map(v =>
      ({ email: v.email, name: v.contactName || v.name, sub: `${v.name} \u00b7 Vendor`, scope: 'external' })),
    ...(ctx.subcontractors || []).filter(x => x.active !== false && x.email).map(x =>
      ({ email: x.email, name: x.contactName || x.companyName, sub: `${x.companyName} \u00b7 Subcontractor`, scope: 'external' })),
    // Any portal login not already reachable through a contact, vendor or
    // subcontractor record — otherwise a client with a login but no contact
    // entry would be unreachable from here.
    ...ctx.teamDirectory.filter(p => p.active && PORTAL_ROLES.includes(p.securityRole) && p.email)
      .map(p => ({ email: p.email, name: p.name, sub: `${p.securityRole} portal`, scope: 'external' })),
  ].map(withPortal)
   .filter((o, i, arr) => arr.findIndex(x => (x.email || '').toLowerCase() === (o.email || '').toLowerCase()) === i);

  const ql = q.trim().toLowerCase();
  const taken = o => picked.some(p => (o.userId && p.userId === o.userId) || (!o.userId && p.email === o.email));
  const options = (tab === 'internal' ? internalOptions : externalOptions)
    .filter(o => !taken(o) && (!ql || [o.name, o.email, o.sub].some(v => (v || '').toLowerCase().includes(ql))));

  const internalPicked = picked.filter(p => p.userId && !p.portal);
  const externalPicked = picked.filter(p => !p.userId || p.portal);

  function add(o) { setPicked(prev => [...prev, o]); setQ(''); }
  function addTyped() {
    const e = extra.trim();
    if (!e || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(e)) return;
    if (picked.some(p => p.email === e)) { setExtra(''); return; }
    const login = portalLoginFor(e);
    setPicked(prev => [...prev, login
      ? { email: e, name: login.name, sub: 'Portal login', scope: 'external', userId: login.id, portal: true }
      : { email: e, name: e, sub: 'Typed address', scope: 'external' }]);
    setExtra('');
  }
  function send() {
    setSent(ctx.shareItem({
      subject, summary, link, projectId, subjectKey, recipients: picked, message,
      items: chosenItems.map(i => ({ id: i.id, label: i.label, group: i.group || null, bytes: i.bytes || null, delivery: i.delivery || null })),
      itemsTotal: pack.length || null,
    }));
  }

  const TABS = [
    { k: 'internal', l: 'Internally', hint: 'Our own people — their app inbox, and email' },
    { k: 'external', l: 'Externally', hint: 'Clients, vendors and anyone else — email only' },
  ];

  return (
    <Modal wide open={open} onClose={onClose} title={sent ? 'Shared' : 'Share \u2014 who should see this?'}
      footer={sent
        ? <Button onClick={onClose}>Done</Button>
        : <><Button variant="ghost" onClick={onClose}>Cancel</Button>
            <Button onClick={send} disabled={!picked.length || (pack.length > 0 && !chosenItems.length)}>
              Share with {picked.length || 'nobody'}{picked.length === 1 ? ' person' : picked.length ? ' people' : ''}
            </Button></>}>
      {sent ? (
        <div className="space-y-2">
          <p className="text-sm"><b>{subject}</b> shared with {picked.map(p => p.name).join(', ')}.</p>
          {pack.length > 0 && (
            <p className="text-xs text-[var(--leon-black)]/55">
              {chosenItems.length} of {pack.length} item{pack.length === 1 ? '' : 's'} included
              {chosenItems.length < pack.length ? ' — the rest were left out.' : '.'}
            </p>
          )}
          {sent.inApp > 0 && (
            <p className="text-sm text-[var(--leon-green)] font-semibold">
              {/* "colleague" was wrong — a subcontractor or client with a portal
                  login lands here too, and they are not colleagues. */}
              &#10003; {sent.inApp} {sent.inApp === 1 ? 'person' : 'people'} can see it in their inbox now.
            </p>
          )}
          {sent.queued > 0 && (
            <>
              <p className="text-sm text-[var(--leon-yellow)] font-semibold">
                &#9888; {sent.queued} email{sent.queued === 1 ? '' : 's'} queued &mdash; not sent.
              </p>
              <p className="text-xs text-[var(--leon-black)]/50">
                Sending email needs a mail service, which needs a backend. The messages are held in the
                outbox and go out once one is connected &mdash; nothing is lost meanwhile.
              </p>
            </>
          )}
          <p className="text-xs text-[var(--leon-black)]/50">This share is now on the record &mdash; open <b>shared {'\u00d7'}</b> beside the button to see the trail.</p>
        </div>
      ) : (
        <div className="space-y-3">
          <div className="border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-[var(--leon-cream)]/60">
            <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">Sharing</p>
            <p className="text-sm font-semibold">{subject}</p>
            {summary && <p className="text-xs text-[var(--leon-black)]/55">{summary}</p>}
          </div>

          {pack.length > 0 && (
            <div className="border border-[var(--leon-line)] rounded-lg">
              <div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--leon-line)] flex-wrap">
                <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">
                  What to include ({chosenItems.length} of {pack.length})
                </p>
                <div className="flex-1" />
                <button onClick={() => setChosen(new Set(pack.map(i => i.id)))} className="text-[11px] font-semibold text-[var(--leon-brown)] hover:underline">All</button>
                <button onClick={() => setChosen(new Set())} className="text-[11px] font-semibold text-[var(--leon-black)]/45 hover:underline">None</button>
              </div>
              {/* Two levels when the package has sections: tick a whole
                  section, or open it and pick individual files. A scope's
                  package is exactly this shape — sections, then their files. */}
              <div className="max-h-60 overflow-y-auto">
                {packGroups.map(g => {
                  const inGroup = pack.filter(i => (i.group || '') === g);
                  const on = inGroup.filter(i => chosen.has(i.id)).length;
                  const allOn = on === inGroup.length;
                  return (
                    <div key={g || '_'} className="border-b border-[var(--leon-line)] last:border-0">
                      {g && (
                        <label className="flex items-center gap-2 px-3 py-1.5 bg-[var(--leon-cream)]/70 cursor-pointer">
                          <input type="checkbox" className="w-3.5 h-3.5 accent-[var(--leon-brown)]"
                            checked={allOn} ref={el => { if (el) el.indeterminate = on > 0 && !allOn; }}
                            onChange={e => setChosen(prev => {
                              const n = new Set(prev);
                              inGroup.forEach(i => { if (e.target.checked) n.add(i.id); else n.delete(i.id); });
                              return n;
                            })} />
                          <span className="text-[11px] font-bold uppercase tracking-wide text-[var(--leon-black)]/60">{g}</span>
                          <span className="text-[10px] text-[var(--leon-black)]/40">{on} of {inGroup.length}</span>
                        </label>
                      )}
                      {inGroup.map(i => (
                        <label key={i.id} className={`flex items-start gap-2 py-1.5 cursor-pointer hover:bg-[var(--leon-cream)]/60 ${g ? 'pl-8 pr-3' : 'px-3'}`}>
                          <input type="checkbox" className="w-3.5 h-3.5 accent-[var(--leon-brown)] mt-0.5"
                            checked={chosen.has(i.id)}
                            onChange={e => setChosen(prev => { const n = new Set(prev); if (e.target.checked) n.add(i.id); else n.delete(i.id); return n; })} />
                          <span className="min-w-0">
                            <span className="block text-xs font-semibold leading-tight truncate">{i.label}</span>
                            {i.sub && <span className="block text-[10px] text-[var(--leon-black)]/45 leading-tight truncate">{i.sub}</span>}
                          </span>
                        </label>
                      ))}
                    </div>
                  );
                })}
              </div>
              {chosenItems.length === 0 && (
                <p className="px-3 py-1.5 text-[11px] text-[var(--leon-red)] font-semibold">Pick at least one item to share.</p>
              )}
            </div>
          )}

          {picked.length > 0 && (
            <div className="space-y-1.5">
              {internalPicked.length > 0 && (
                <div>
                  <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-green)] mb-1">Internal &mdash; inbox + email ({internalPicked.length})</p>
                  <div className="flex flex-wrap gap-1.5">
                    {internalPicked.map((p, i) => (
                      <span key={'i' + i} className="inline-flex items-center gap-1.5 px-2 py-1 rounded-lg border border-[var(--leon-green)]/50 bg-[var(--leon-green)]/5 text-xs font-semibold">
                        {p.name}
                        <button onClick={() => setPicked(prev => prev.filter(x => x !== p))} title="Remove">&#10005;</button>
                      </span>
                    ))}
                  </div>
                </div>
              )}
              {externalPicked.length > 0 && (
                <div>
                  <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-yellow)] mb-1">
                    External ({externalPicked.length}) &mdash; those with a portal login also get it in their portal inbox
                  </p>
                  <div className="flex flex-wrap gap-1.5">
                    {externalPicked.map((p, i) => (
                      <span key={'e' + i} className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-lg border text-xs font-semibold ${p.portal ? 'border-[var(--leon-brown)]/60 bg-[var(--leon-cream)]' : 'border-[var(--leon-yellow)]/60 bg-[var(--leon-yellow)]/5'}`}>
                        {p.name}
                        <span className="font-normal opacity-70">{p.portal ? 'portal + email' : p.email}</span>
                        <button onClick={() => setPicked(prev => prev.filter(x => x !== p))} title="Remove">&#10005;</button>
                      </span>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}

          <div className="flex gap-1 border-b border-[var(--leon-line)]">
            {TABS.map(t => (
              <button key={t.k} onClick={() => { setTab(t.k); setQ(''); }}
                className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${tab === t.k ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
                {t.l}{t.k === 'internal' && internalPicked.length ? ` (${internalPicked.length})` : ''}{t.k === 'external' && externalPicked.length ? ` (${externalPicked.length})` : ''}
              </button>
            ))}
          </div>
          <p className="text-xs text-[var(--leon-black)]/50 -mt-1">{TABS.find(t => t.k === tab).hint}</p>

          <TextInput value={q} onChange={e => setQ(e.target.value)}
            placeholder={tab === 'internal' ? 'Search a colleague by name, title or role…' : 'Search a client contact, vendor or subcontractor…'} />

          <div className="border border-[var(--leon-line)] rounded-lg max-h-48 overflow-y-auto">
            {options.length === 0 ? (
              <p className="px-3 py-4 text-center text-sm text-[var(--leon-black)]/40">
                {tab === 'external' && !project
                  ? 'Open this from a project to see its client contacts \u2014 or type an address below.'
                  : 'No one else matches.'}
              </p>
            ) : options.slice(0, 40).map((o, i) => (
              <button key={(o.userId || o.email) + i} onClick={() => add(o)}
                className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[var(--leon-cream)] border-b border-[var(--leon-line)] last:border-0">
                <Avatar name={o.name} size={26} />
                <span className="min-w-0">
                  <span className="block text-sm font-semibold leading-tight truncate">{o.name}</span>
                  <span className="block text-[11px] text-[var(--leon-black)]/45 leading-tight truncate">{o.sub}{o.email ? ` \u00b7 ${o.email}` : ''}</span>
                </span>
                <span className="flex-1" />
                {o.portal && <span className="text-[10px] font-semibold text-[var(--leon-brown)] whitespace-nowrap">has portal</span>}
              </button>
            ))}
          </div>

          {tab === 'external' && (
            <div className="flex items-end gap-2">
              <Field label="Or type an email address" className="!mb-0 flex-1">
                <TextInput value={extra} onChange={e => setExtra(e.target.value)}
                  onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addTyped(); } }}
                  placeholder="name@company.com" />
              </Field>
              <Button variant="outline" onClick={addTyped} disabled={!extra.trim()}>Add</Button>
            </div>
          )}

          <Field label="Message" hint="Optional — say why you are sending it">
            <TextArea rows={3} value={message} onChange={e => setMessage(e.target.value)}
              placeholder="e.g. this is the schedule we agreed on Tuesday — the install week moved" />
          </Field>

          {/* Exactly the text that will be queued — same composer, so the
              preview cannot drift from what actually sends. */}
          {picked.length > 0 && (() => {
            const first = picked[0];
            const em = buildShareEmail({
              recipientName: first.name, senderName: ctx.currentUserName, senderTitle: personTitle(ctx.currentUser),
              senderSignature: ctx.currentUser && ctx.currentUser.emailSignature,
    senderSignatureImage: ctx.currentUser && ctx.currentUser.emailSignatureImage,
              subject, summary, message, items: chosenItems, itemsTotal: pack.length || null,
              projectName: project ? project.name : '', company: ctx.companyProfile, external: !first.userId || !!first.portal,
            });
            return (
              <details className="border border-[var(--leon-line)] rounded-lg">
                <summary className="px-3 py-2 text-xs font-semibold text-[var(--leon-brown)] cursor-pointer">
                  Preview the email {picked.length > 1 ? `(as ${first.name} would get it)` : ''}
                </summary>
                <div className="px-3 pb-3">
                  <div className="border border-[var(--leon-line)] rounded-md bg-white overflow-hidden">
                    <div className="px-3 py-2 border-b border-[var(--leon-line)] bg-[var(--leon-cream)]/60 text-[11px]">
                      <p><span className="text-[var(--leon-black)]/45">To</span> <b>{first.email || '—'}</b></p>
                      {/* The sender is always copied — this is their record of
                          what went out, and it belongs in their own mailbox. */}
                      <p><span className="text-[var(--leon-black)]/45">Cc</span> <b>{(ctx.currentUser && ctx.currentUser.email) || '—'}</b> <span className="text-[var(--leon-black)]/40">(you)</span></p>
                      <p><span className="text-[var(--leon-black)]/45">Subject</span> <b>{em.subject}</b></p>
                    </div>
                    {/* The preview renders the SAME html the queue carries, in
                        a sandboxed iframe so the email's own styling cannot
                        leak into the app or the app's into it. */}
                    {previewText
                      ? <pre className="px-3 py-2 text-[11px] leading-relaxed whitespace-pre-wrap font-sans">{em.body}</pre>
                      : <iframe title="Email preview" sandbox="" srcDoc={em.html} className="w-full h-80 border-0 bg-white" />}
                  </div>
                  <button type="button" onClick={() => setPreviewText(v => !v)}
                    className="mt-1 text-[10px] font-semibold text-[var(--leon-brown)]">
                    {previewText ? 'Show the branded version' : 'Show the plain-text version'}
                  </button>
                  <p className="text-[10px] text-[var(--leon-black)]/45 mt-1.5">
                    {em.attachCount > 0 && <>{em.attachCount} file{em.attachCount === 1 ? '' : 's'} will be attached. </>}
                    {em.linkCount > 0 && <><b>{em.linkCount} file{em.linkCount === 1 ? '' : 's'} ({fmtBytes(em.linkBytes)}) {em.linkCount === 1 ? 'is' : 'are'} too large to attach and will be sent as a download link.</b> </>}
                    Files go on once a mail service is connected &mdash; a browser cannot attach and send a file on its own.
                  </p>
                </div>
              </details>
            );
          })()}
        </div>
      )}
    </Modal>
  );
}

// ============================================================================
// Quality Control — areas, scheduled inspections, and the result
// ============================================================================
// Some inspections are ours and some are a third party's; both are the SAME
// record with a different person on it, so a job's QC history reads the same
// however the inspection was done. Scheduling and editing belongs to the
// Production Director and the associates (module 'qc'); everyone else sees it
// read-only, which is what `editable` carries down from the tab.
const QC_RESULT_TONE = { 'Passed': 'green', 'Passed with Notes': 'yellow', 'Failed': 'red', 'Cancelled': 'neutral', 'Scheduled': 'blue', 'In Progress': 'blue' };
function qcInspectorName(ctx, insp) {
  if (!insp.inspectorId) return 'Unassigned';
  const p = ctx.teamDirectory.find(x => x.id === insp.inspectorId);
  return p ? p.name : 'Unassigned';
}
function QualityControlTab({ ctx, project }) {
  const editable = ctx.canEdit('qc');
  const [addArea, setAddArea] = useState(false);
  const [editArea, setEditArea] = useState(null);
  const [schedule, setSchedule] = useState(false);
  const [open, setOpen] = useState(null);
  const [fStatus, setFStatus] = useState('open');
  const areas = (project.qcAreas || []).filter(a => a.active !== false);
  const all = project.qcInspections || [];
  const shown = all.filter(i => fStatus === 'all' || (fStatus === 'open' ? qcInspectionOpen(i) : !qcInspectionOpen(i)));
  const openInsp = all.find(i => i.id === open) || null;

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        Inspections are booked against an <b>area</b> of the job and carried out either in-house or by a
        third-party inspector, who records the result from their own portal. Scheduling and editing is
        the Production Director&rsquo;s and the associates&rsquo;; everyone else can see where QC stands.
        {!editable && <span className="block mt-1 text-[var(--leon-yellow)] font-semibold">You have view-only access here.</span>}
      </p>

      <Collapsible title="Areas" count={areas.length}
        right={editable && <Button size="sm" variant="ghost" onClick={() => setAddArea(true)}>+ Add Area</Button>}>
        {areas.length === 0 ? <EmptyState text="No areas set up yet. An area is the part of the job an inspection covers — a floor, a unit, a room, a run of casework." /> : (
          <div className="divide-y divide-[var(--leon-line)]">
            {areas.map(a => {
              const sc = project.scopes.find(s => s.id === a.scopeId);
              const n = all.filter(i => i.areaId === a.id).length;
              return (
                <div key={a.id} className="flex items-center gap-3 py-2 text-sm">
                  <span className="font-semibold">{a.name}</span>
                  {sc && <Badge tone="neutral">{sc.name}</Badge>}
                  {a.description && <span className="text-xs text-[var(--leon-black)]/50 truncate">{a.description}</span>}
                  <div className="flex-1" />
                  <span className="text-xs text-[var(--leon-black)]/40">{n} inspection{n === 1 ? '' : 's'}</span>
                  {editable && <>
                    <button onClick={() => setEditArea(a)} className="text-xs text-[var(--leon-brown)] font-semibold">Edit</button>
                    <IconBtn title="Retire this area" onClick={() => { if (confirm(`Retire "${a.name}"? Inspections already booked against it are kept.`)) ctx.removeQcArea(project.id, a.id); }}>&#10005;</IconBtn>
                  </>}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>

      <div className="flex items-center gap-2 flex-wrap mb-2 mt-4">
        <h2 className="text-sm font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Inspections</h2>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {[{ k: 'open', l: `Open (${all.filter(qcInspectionOpen).length})` }, { k: 'done', l: `Completed (${all.filter(i => !qcInspectionOpen(i)).length})` }, { k: 'all', l: `All (${all.length})` }].map(o => (
            <button key={o.k} onClick={() => setFStatus(o.k)}
              className={`px-2.5 py-1 rounded-md text-xs font-semibold ${fStatus === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
          ))}
        </div>
        <div className="flex-1" />
        {editable && <Button size="sm" onClick={() => setSchedule(true)} disabled={!areas.length}
          title={areas.length ? '' : 'Set up an area first — an inspection is booked against one.'}>+ Schedule Inspection</Button>}
      </div>
      {shown.length === 0 ? <EmptyState text={all.length ? 'Nothing in this view.' : 'No inspections scheduled yet.'} /> : (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                <th className="px-3 py-2">Date</th><th className="px-3 py-2">Area</th><th className="px-3 py-2">Inspection</th>
                <th className="px-3 py-2">Inspector</th><th className="px-3 py-2">Checks</th><th className="px-3 py-2">Status</th><th className="px-3 py-2"></th>
              </tr>
            </thead>
            <tbody>
              {shown.map(i => {
                const area = areas.find(a => a.id === i.areaId) || (project.qcAreas || []).find(a => a.id === i.areaId);
                const done = i.checklist.filter(c => c.result).length;
                return (
                  <tr key={i.id} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-1.5 whitespace-nowrap">{fmtDate(i.scheduledDate)}</td>
                    <td className="px-3 py-1.5">{area ? area.name : '—'}</td>
                    <td className="px-3 py-1.5 font-semibold">{i.title || '—'}</td>
                    <td className="px-3 py-1.5">
                      {qcInspectorName(ctx, i)}
                      <span className="ml-1.5 text-[10px] text-[var(--leon-black)]/40">{i.inspectorType}</span>
                    </td>
                    <td className="px-3 py-1.5 tabular-nums">{i.checklist.length ? `${done}/${i.checklist.length}` : '—'}</td>
                    <td className="px-3 py-1.5"><Badge tone={QC_RESULT_TONE[i.status] || 'neutral'}>{i.status}</Badge></td>
                    <td className="px-3 py-1.5 text-right whitespace-nowrap">
                      <button onClick={() => setOpen(i.id)} className="text-[var(--leon-brown)] font-semibold">Open</button>
                      {editable && qcInspectionOpen(i) && (
                        <IconBtn title="Cancel this inspection" onClick={() => { if (confirm('Cancel this inspection?')) ctx.removeQcInspection(project.id, i.id); }}>&#10005;</IconBtn>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      <QcAreaModal open={addArea || !!editArea} area={editArea} project={project} ctx={ctx}
        onClose={() => { setAddArea(false); setEditArea(null); }} />
      <QcScheduleModal open={schedule} project={project} ctx={ctx} areas={areas} onClose={() => setSchedule(false)} />
      <QcInspectionModal insp={openInsp} project={project} ctx={ctx} editable={editable} onClose={() => setOpen(null)} />
    </div>
  );
}

function QcAreaModal({ open, area, project, ctx, onClose }) {
  const blank = { name: '', scopeId: '', description: '' };
  const [f, setF] = useState(blank);
  useEffect(() => { if (open) setF(area ? { name: area.name, scopeId: area.scopeId || '', description: area.description || '' } : blank); }, [open, area && area.id]);
  function submit() {
    if (!f.name.trim()) return;
    const payload = { name: f.name.trim(), scopeId: f.scopeId || null, description: f.description.trim() };
    if (area) ctx.updateQcArea(project.id, area.id, payload); else ctx.addQcArea(project.id, payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={area ? 'Edit area' : 'Add an area'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!f.name.trim()}>{area ? 'Save' : 'Add area'}</Button></>}>
      <div className="space-y-3">
        <Field label="Area name" hint="What an inspection is booked against — a floor, a unit, a room, a run of casework.">
          <TextInput value={f.name} onChange={e => setF({ ...f, name: e.target.value })} placeholder="e.g. Unit 12B — Kitchen" />
        </Field>
        <Field label="Scope (optional)" hint="Ties the area to one scope, so its QC history sits with that work.">
          <Select value={f.scopeId} onChange={e => setF({ ...f, scopeId: e.target.value })}>
            <option value="">— not scope-specific —</option>
            {project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </Select>
        </Field>
        <Field label="Notes (optional)">
          <TextInput value={f.description} onChange={e => setF({ ...f, description: e.target.value })} placeholder="e.g. includes the island and the pantry run" />
        </Field>
      </div>
    </Modal>
  );
}

function QcScheduleModal({ open, project, ctx, areas, onClose }) {
  const blank = { areaId: '', title: '', inspectorType: 'Third Party', inspectorId: '', scheduledDate: todayISO(), checklist: '' };
  const [f, setF] = useState(blank);
  useEffect(() => { if (open) setF({ ...blank, areaId: (areas[0] || {}).id || '' }); }, [open]);
  // A third-party inspection goes to someone holding a QC Inspector login —
  // that is what gives them the portal. An in-house one goes to our own people.
  const people = ctx.teamDirectory.filter(p => p.active && (f.inspectorType === 'Third Party'
    ? p.securityRole === 'QC Inspector'
    : p.securityRole !== 'QC Inspector' && !['Client', 'Subcontractor', 'Delivery Driver'].includes(p.securityRole)));
  const ready = f.areaId && f.title.trim() && f.scheduledDate && f.inspectorId;
  function submit() {
    const area = areas.find(a => a.id === f.areaId);
    ctx.addQcInspection(project.id, {
      areaId: f.areaId, scopeId: area ? area.scopeId : null, title: f.title.trim(),
      inspectorType: f.inspectorType, inspectorId: f.inspectorId, scheduledDate: f.scheduledDate,
      checklist: f.checklist.split('\n').map(x => x.trim()).filter(Boolean),
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Schedule an inspection"
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!ready}>Schedule</Button></>}>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Area">
            <Select value={f.areaId} onChange={e => setF({ ...f, areaId: e.target.value })}>
              <option value="">— choose an area —</option>
              {areas.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
            </Select>
          </Field>
          <Field label="Date"><TextInput type="date" value={f.scheduledDate} onChange={e => setF({ ...f, scheduledDate: e.target.value })} /></Field>
        </div>
        <Field label="What is being inspected">
          <TextInput value={f.title} onChange={e => setF({ ...f, title: e.target.value })} placeholder="e.g. Cabinet installation — final" />
        </Field>
        <Field label="Who is inspecting" hint="A third party records the result from their own portal; an in-house inspection is recorded here.">
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit mb-2">
            {QC_INSPECTOR_TYPES.map(t => (
              <button key={t} type="button" onClick={() => setF({ ...f, inspectorType: t, inspectorId: '' })}
                className={`px-3 py-1.5 rounded-md text-sm font-semibold ${f.inspectorType === t ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>
                {t === 'Third Party' ? '🔍 Third Party' : '🏢 In-house'}
              </button>
            ))}
          </div>
          <Select value={f.inspectorId} onChange={e => setF({ ...f, inspectorId: e.target.value })}>
            <option value="">— choose an inspector —</option>
            {people.map(p => <option key={p.id} value={p.id}>{p.name}{p.title ? ` — ${p.title}` : ''}</option>)}
          </Select>
          {f.inspectorType === 'Third Party' && people.length === 0 && (
            <p className="text-[11px] text-[var(--leon-yellow)] font-semibold mt-1">
              No third-party inspectors yet. Add one under Users with the <b>QC Inspector</b> role — that login is what gives them the portal.
            </p>
          )}
        </Field>
        <Field label="Checklist (optional)" hint="One check per line. The inspector marks each pass or fail and can add their own.">
          <TextArea rows={4} value={f.checklist} onChange={e => setF({ ...f, checklist: e.target.value })}
            placeholder={'Doors aligned and gaps even\nHardware fitted and adjusted\nNo damage to finished faces\nSilicone / caulk lines clean'} />
        </Field>
      </div>
    </Modal>
  );
}

// The inspection itself. The same component serves the internal tab and the
// inspector's portal — `editable` decides whether the checks can be marked, so
// a coordinator reading it cannot silently change an inspector's findings.
function QcInspectionModal({ insp, project, ctx, editable, onClose, canRecord }) {
  const [newCheck, setNewCheck] = useState('');
  const [result, setResult] = useState('');
  const [notes, setNotes] = useState('');
  useEffect(() => { if (insp) { setResult(insp.result || ''); setNotes(insp.notes || ''); setNewCheck(''); } }, [insp && insp.id]);
  if (!insp) return null;
  const area = (project.qcAreas || []).find(a => a.id === insp.areaId);
  const record = canRecord !== undefined ? canRecord : editable;
  const openStill = qcInspectionOpen(insp);
  const needsNote = QC_RESULTS_NEEDING_NOTES.includes(result);
  const canSubmit = record && openStill && result && (!needsNote || notes.trim());
  return (
    <Modal wide open={!!insp} onClose={onClose} title={insp.title || 'Inspection'}
      footer={<><Button variant="ghost" onClick={onClose}>Close</Button>
               {record && openStill && (
                 <Button onClick={() => { ctx.submitQcResult(project.id, insp.id, result, notes.trim()); onClose(); }} disabled={!canSubmit}>
                   {!result ? 'Choose a result' : needsNote && !notes.trim() ? 'Add a note first' : `Record ${result}`}
                 </Button>
               )}</>}>
      <div className="space-y-4">
        <div className="flex flex-wrap items-center gap-2 text-xs text-[var(--leon-black)]/55">
          <Badge tone={QC_RESULT_TONE[insp.status] || 'neutral'}>{insp.status}</Badge>
          <span>{area ? area.name : 'No area'}</span><span>·</span>
          <span>{fmtDate(insp.scheduledDate)}</span><span>·</span>
          <span>{qcInspectorName(ctx, insp)} ({insp.inspectorType})</span>
          {insp.completedDate && <><span>·</span><span>completed {fmtDate(insp.completedDate)}</span></>}
        </div>

        <div>
          <div className="flex items-center gap-2 mb-1.5">
            <h3 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Checks</h3>
            <span className="text-xs text-[var(--leon-black)]/40">{insp.checklist.filter(c => c.result).length} of {insp.checklist.length} marked</span>
          </div>
          {insp.checklist.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40 italic">No checks listed.</p> : (
            <div className="border border-[var(--leon-line)] rounded-lg divide-y divide-[var(--leon-line)]">
              {insp.checklist.map(c => (
                <div key={c.id} className="px-2.5 py-2">
                  <div className="flex items-center gap-2 flex-wrap">
                    <span className="flex-1 min-w-0 text-sm">{c.item}</span>
                    {record && openStill ? (
                      <div className="flex gap-1">
                        {['Pass', 'Fail', 'N/A'].map(r => (
                          <button key={r} type="button" onClick={() => ctx.setQcCheckResult(project.id, insp.id, c.id, { result: c.result === r ? '' : r })}
                            className={`px-2 py-0.5 rounded-md text-[11px] font-semibold border ${c.result === r
                              ? (r === 'Pass' ? 'bg-[#e7f3e9] border-[#3a7d44] text-[#3a7d44]' : r === 'Fail' ? 'bg-[#fbe7e7] border-[var(--leon-red)] text-[var(--leon-red)]' : 'bg-[var(--leon-line)] border-[var(--leon-black)]/30')
                              : 'border-[var(--leon-line)] text-[var(--leon-black)]/50'}`}>{r}</button>
                        ))}
                      </div>
                    ) : c.result ? <Badge tone={c.result === 'Pass' ? 'green' : c.result === 'Fail' ? 'red' : 'neutral'}>{c.result}</Badge>
                      : <span className="text-[11px] text-[var(--leon-black)]/30">not marked</span>}
                    {record && openStill && <IconBtn title="Remove this check" onClick={() => ctx.removeQcCheckItem(project.id, insp.id, c.id)}>&#10005;</IconBtn>}
                  </div>
                  {record && openStill ? (
                    <TextInput value={c.note || ''} onChange={e => ctx.setQcCheckResult(project.id, insp.id, c.id, { note: e.target.value })}
                      placeholder="Note (optional)" className="!py-0.5 !text-xs mt-1" />
                  ) : c.note ? <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{c.note}</p> : null}
                </div>
              ))}
            </div>
          )}
          {record && openStill && (
            <div className="flex items-center gap-2 mt-2">
              <TextInput value={newCheck} onChange={e => setNewCheck(e.target.value)} placeholder="Add a check…" className="!py-1 !text-xs flex-1" />
              <Button size="sm" variant="outline" disabled={!newCheck.trim()}
                onClick={() => { ctx.addQcCheckItem(project.id, insp.id, newCheck.trim()); setNewCheck(''); }}>+ Add</Button>
            </div>
          )}
        </div>

        <div>
          <h3 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">Photos</h3>
          {insp.photos.length === 0 && !record && <p className="text-xs text-[var(--leon-black)]/40 italic">No photos.</p>}
          <div className="flex flex-wrap gap-2">
            {insp.photos.map(ph => (
              <div key={ph.id} className="relative">
                <Photo src={ph.url} alt={ph.name} title={ph.name} className="w-20 h-20 object-cover rounded-lg border border-[var(--leon-line)]" />
                {record && openStill && <button onClick={() => ctx.removeQcPhoto(project.id, insp.id, ph.id)}
                  className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-[var(--leon-red)] text-white text-[10px]">✕</button>}
              </div>
            ))}
          </div>
          {record && openStill && (
            <div className="mt-2">
              <FileField name={null} url={null} editable placeholder="Add a photo"
                onChange={(name, url) => ctx.addQcPhoto(project.id, insp.id, name, url)} />
            </div>
          )}
        </div>

        <div>
          <h3 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">Result</h3>
          {!openStill ? (
            <div className="border border-[var(--leon-line)] rounded-lg px-3 py-2">
              <Badge tone={QC_RESULT_TONE[insp.status] || 'neutral'}>{insp.status}</Badge>
              {insp.notes && <p className="text-sm mt-1.5">{insp.notes}</p>}
              <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">Recorded {fmtDate(insp.completedDate)} by {(insp.activityLog[insp.activityLog.length - 1] || {}).by || qcInspectorName(ctx, insp)}</p>
            </div>
          ) : record ? (
            <div className="space-y-2">
              <div className="flex gap-1 flex-wrap">
                {QC_INSPECTION_RESULTS.map(r => (
                  <button key={r} type="button" onClick={() => setResult(r)}
                    className={`px-3 py-1.5 rounded-lg text-xs font-semibold border ${result === r ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60'}`}>{r}</button>
                ))}
              </div>
              <TextArea rows={3} value={notes} onChange={e => setNotes(e.target.value)}
                placeholder={needsNote ? 'Required — what was wrong, and where' : 'Notes (optional)'} />
              {needsNote && !notes.trim() && <p className="text-[11px] text-[var(--leon-yellow)] font-semibold">A result other than a clean pass needs a note — the shop has to know what to act on.</p>}
            </div>
          ) : <p className="text-xs text-[var(--leon-black)]/40 italic">Waiting on the inspector.</p>}
        </div>
      </div>
    </Modal>
  );
}

// ============================================================================
// QC Inspector portal — a third party's whole view of the app
// ============================================================================
// Deliberately narrow: the inspections booked in their name, and nothing else.
// They see no financials, no other projects, no other people's work.
function QCPortal({ ctx }) {
  const [open, setOpen] = useState(null);
  const me = ctx.currentUser.id;
  const rows = [];
  ctx.projects.forEach(p => (p.qcInspections || []).forEach(i => {
    if (i.inspectorId === me) rows.push({ project: p, insp: i });
  }));
  rows.sort((a, b) => (a.insp.scheduledDate < b.insp.scheduledDate ? -1 : 1));
  const upcoming = rows.filter(r => qcInspectionOpen(r.insp));
  const done = rows.filter(r => !qcInspectionOpen(r.insp));
  const current = rows.find(r => r.insp.id === open) || null;

  return (
    <div className="min-h-screen bg-[var(--leon-cream)]">
      <header className="bg-white border-b border-[var(--leon-line)]">
        <div className="max-w-5xl mx-auto px-4 md:px-6 h-20 flex items-center gap-3">
          <img src="logo/leon-wordmark.svg" alt="LEON" className="h-6" />
          <span className="text-[9px] tracking-[0.35em] text-[var(--leon-brown)] font-semibold">QUALITY CONTROL</span>
          <div className="flex-1" />
          <PortalInbox ctx={ctx} />
          <div className="text-right leading-tight">
            <p className="text-[9px] uppercase tracking-wide text-[var(--leon-black)]/40">Signed in as</p>
            <p className="text-xs font-semibold">{ctx.currentUser.name}</p>
          </div>
          <button onClick={ctx.logout} className="text-sm text-[var(--leon-brown)] font-semibold">Log Out</button>
        </div>
      </header>
      <main className="max-w-5xl mx-auto px-4 md:px-6 py-6">
        <h1 className="text-xl font-bold mb-1">My Inspections</h1>
        <p className="text-sm text-[var(--leon-black)]/50 mb-5">
          Everything booked in your name. Open one to work through the checks, add photos and record the result.
        </p>
        {rows.length === 0 ? <EmptyState text="Nothing booked in your name yet." /> : (
          <>
            <h2 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">To do ({upcoming.length})</h2>
            {upcoming.length === 0 ? <p className="text-sm text-[var(--leon-black)]/40 italic mb-5">Nothing outstanding.</p> : (
              <div className="space-y-2 mb-6">
                {upcoming.map(r => <QcPortalRow key={r.insp.id} ctx={ctx} row={r} onOpen={() => setOpen(r.insp.id)} />)}
              </div>
            )}
            {done.length > 0 && (
              <>
                <h2 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">Completed ({done.length})</h2>
                <div className="space-y-2">
                  {done.map(r => <QcPortalRow key={r.insp.id} ctx={ctx} row={r} onOpen={() => setOpen(r.insp.id)} />)}
                </div>
              </>
            )}
          </>
        )}
        {current && (
          <QcInspectionModal insp={current.insp} project={current.project} ctx={ctx}
            editable={false} canRecord={qcInspectionOpen(current.insp)} onClose={() => setOpen(null)} />
        )}
      </main>
      <AppFooter />
    </div>
  );
}
function QcPortalRow({ ctx, row, onOpen }) {
  const { project, insp } = row;
  const area = (project.qcAreas || []).find(a => a.id === insp.areaId);
  const marked = insp.checklist.filter(c => c.result).length;
  return (
    <button onClick={onOpen} className="w-full text-left bg-white border border-[var(--leon-line)] rounded-xl px-4 py-3 hover:border-[var(--leon-brown-light)]">
      <div className="flex items-center gap-2 flex-wrap">
        <span className="text-sm font-bold">{insp.title || 'Inspection'}</span>
        <Badge tone={QC_RESULT_TONE[insp.status] || 'neutral'}>{insp.status}</Badge>
        <div className="flex-1" />
        <span className="text-xs text-[var(--leon-black)]/45">{fmtDate(insp.scheduledDate)}</span>
      </div>
      <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">
        {project.name}{area ? ` · ${area.name}` : ''}
        {insp.checklist.length ? ` · ${marked}/${insp.checklist.length} checks marked` : ''}
      </p>
    </button>
  );
}

// ============================================================================
// Notifications — bell, inbox, preferences
// ============================================================================
// The inbox shows two lists that come from opposite directions: EVENTS that
// were recorded when someone acted, and ALERTS recomputed from your own work
// every time this renders. Keeping them visually distinct matters — one is
// history you can clear, the other is a live state of the world you cannot.
function myNotifications(ctx) {
  return (ctx.notifications || []).filter(n => n.toUserId === ctx.currentUserId);
}
function NotificationBell({ ctx }) {
  const [open, setOpen] = useState(false);
  const mine = myNotifications(ctx);
  const unread = mine.filter(n => !n.read);
  const alerts = useMemo(() => buildDueAlerts(collectMyItems(ctx, ctx.currentUserId), todayISO()),
    [ctx.projects, ctx.personalItems, ctx.materialAllocations, ctx.currentUserId]);
  const badge = unread.length + alerts.filter(a => a.level === 'late').length;
  return (
    <div className="relative shrink-0" onClick={e => e.stopPropagation()}>
      <button onClick={() => setOpen(o => !o)} title="Notifications"
        className={`relative w-9 h-9 rounded-full grid place-items-center text-lg transition ${open ? 'bg-[var(--leon-cream)]' : 'hover:bg-[var(--leon-cream)]'}`}>
        <span aria-hidden="true">&#128276;</span>
        {badge > 0 && (
          <span className="absolute -top-0.5 -right-0.5 min-w-[17px] h-[17px] px-1 rounded-full bg-[var(--leon-red)] text-white text-[10px] font-bold grid place-items-center">
            {badge > 99 ? '99+' : badge}
          </span>
        )}
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-1 z-40 w-96 max-w-[92vw] bg-white border border-[var(--leon-line)] rounded-xl shadow-xl overflow-hidden">
          <div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--leon-line)]">
            <p className="text-sm font-bold">Notifications</p>
            <div className="flex-1" />
            {unread.length > 0 && (
              <button onClick={() => ctx.markAllNotificationsRead()} className="text-[11px] font-semibold text-[var(--leon-brown)] hover:underline">Mark all read</button>
            )}
          </div>
          <div className="max-h-96 overflow-y-auto">
            {alerts.slice(0, 4).map(a => <AlertRow key={a.id} alert={a} />)}
            {mine.slice(0, 8).map(n => (
              <NotificationRow key={n.id} ctx={ctx} n={n} onGo={() => { setOpen(false); }} />
            ))}
            {!alerts.length && !mine.length && (
              <p className="px-3 py-6 text-center text-sm text-[var(--leon-black)]/40">Nothing needs you right now.</p>
            )}
          </div>
          <button onClick={() => { setOpen(false); ctx.goInbox(); }}
            className="w-full px-3 py-2 text-xs font-bold uppercase tracking-wide text-[var(--leon-brown)] border-t border-[var(--leon-line)] hover:bg-[var(--leon-cream)]">
            Open inbox
          </button>
        </div>
      )}
    </div>
  );
}
function AlertRow({ alert }) {
  const late = alert.level === 'late';
  return (
    <div className={`flex items-start gap-2 px-3 py-2 border-b border-[var(--leon-line)] ${late ? 'bg-[var(--leon-red)]/5' : 'bg-[var(--leon-yellow)]/5'}`}>
      <span className={`w-5 h-5 rounded-full grid place-items-center text-[11px] font-bold shrink-0 ${late ? 'bg-[var(--leon-red)]/15 text-[var(--leon-red)]' : 'bg-[var(--leon-yellow)]/20 text-[var(--leon-yellow)]'}`}>
        {late ? '!' : '\u26a0'}
      </span>
      <div className="min-w-0">
        <p className="text-sm font-semibold leading-snug">{alert.title}</p>
        <p className="text-[11px] text-[var(--leon-black)]/50">
          {alert.kind}{alert.projectName ? ` \u00b7 ${alert.projectName}` : ''} &middot;
          <b className={late ? 'text-[var(--leon-red)]' : 'text-[var(--leon-yellow)]'}> {alert.label}</b>
        </p>
      </div>
    </div>
  );
}
function NotificationRow({ ctx, n, onGo }) {
  const ev = notificationEvent(n.event);
  function go() {
    ctx.markNotificationRead(n.id, true);
    if (n.link && n.link.view === 'project' && n.link.projectId) {
      if (n.link.tab) ctx.goProjectTab(n.link.projectId, n.link.tab); else ctx.goProject(n.link.projectId);
    }
    if (onGo) onGo();
  }
  return (
    <div className={`flex items-start gap-2 px-3 py-2 border-b border-[var(--leon-line)] ${n.read ? '' : 'bg-[var(--leon-cream)]/60'}`}>
      {!n.read && <span className="w-1.5 h-1.5 rounded-full bg-[var(--leon-brown)] mt-2 shrink-0" />}
      <button onClick={go} className="min-w-0 text-left flex-1">
        <p className="text-sm font-semibold leading-snug">{n.title}</p>
        {n.body && <p className="text-[11px] text-[var(--leon-black)]/55 leading-snug">{n.body}</p>}
        <p className="text-[10px] text-[var(--leon-black)]/35 mt-0.5">
          {[ev ? ev.label : n.event, n.projectName, fmtDate(n.date)].filter(Boolean).join(' \u00b7 ')}
        </p>
      </button>
      <button onClick={() => ctx.markNotificationRead(n.id, !n.read)} title={n.read ? 'Mark unread' : 'Mark read'}
        className="text-[10px] text-[var(--leon-black)]/35 hover:underline shrink-0">{n.read ? 'unread' : 'read'}</button>
    </div>
  );
}

function InboxView({ ctx }) {
  const [tab, setTab] = useState('all');
  const mine = myNotifications(ctx);
  const items = useMemo(() => collectMyItems(ctx, ctx.currentUserId), [ctx.projects, ctx.personalItems, ctx.materialAllocations, ctx.currentUserId]);
  const alerts = useMemo(() => buildDueAlerts(items, todayISO()), [items]);
  const shown = tab === 'unread' ? mine.filter(n => !n.read) : mine;
  const myQueue = (ctx.emailOutbox || []).filter(m => m.toUserId === ctx.currentUserId);
  const [openMail, setOpenMail] = useState(null);

  function exportIcs() {
    downloadIcs(`leon-my-work-${todayISO()}`, buildIcsForItems(items, ctx.currentUserName));
  }

  return (
    <div>
      <div className="flex items-start justify-between gap-3 flex-wrap mb-4">
        <div>
          <h1 className="text-2xl font-bold mb-1">Inbox</h1>
          <p className="text-sm text-[var(--leon-black)]/50 max-w-2xl">
            What has happened to your work, and what is about to come due. Deadlines are
            recalculated every time you open this &mdash; they can&rsquo;t go stale.
          </p>
        </div>
        <div className="flex items-center gap-2">
          <Button variant="outline" onClick={exportIcs}>&#128197; Add to Outlook (.ics)</Button>
        </div>
      </div>

      {alerts.length > 0 && (
        <div className="mb-5">
          <h2 className="text-sm font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">
            Coming due &amp; overdue ({alerts.length})
          </h2>
          <div className="border border-[var(--leon-line)] rounded-xl overflow-hidden bg-white">
            {alerts.map(a => <AlertRow key={a.id} alert={a} />)}
          </div>
        </div>
      )}

      <div className="flex items-center gap-2 mb-2 flex-wrap">
        <h2 className="text-sm font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Activity</h2>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {[{ k: 'all', l: `All (${mine.length})` }, { k: 'unread', l: `Unread (${mine.filter(n => !n.read).length})` }].map(o => (
            <button key={o.k} onClick={() => setTab(o.k)}
              className={`px-2.5 py-1 rounded-md text-xs font-semibold ${tab === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
          ))}
        </div>
        <div className="flex-1" />
        {mine.some(n => !n.read) && <button onClick={ctx.markAllNotificationsRead} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline">Mark all read</button>}
        {mine.some(n => n.read) && <button onClick={ctx.clearReadNotifications} className="text-xs font-semibold text-[var(--leon-black)]/45 hover:underline">Clear read</button>}
      </div>
      {shown.length === 0 ? <EmptyState text="Nothing here yet." /> : (
        <div className="border border-[var(--leon-line)] rounded-xl overflow-hidden bg-white">
          {shown.map(n => <NotificationRow key={n.id} ctx={ctx} n={n} />)}
        </div>
      )}

      {/* The email side is honest about where it stands: rules run, messages
          queue, and nothing claims to have been sent. */}
      {myQueue.length > 0 && (
        <Collapsible title="Email queue" count={myQueue.length}>
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">
            These matched your email preferences. They are <b>queued, not sent</b> &mdash; sending
            email needs a mail service, which needs a backend. When one is connected, this queue
            is what it delivers; nothing is lost meanwhile.
          </p>
          <div className="space-y-1">
            {myQueue.slice(0, 30).map(m => (
              <div key={m.id} className="flex items-center gap-2 text-xs border border-[var(--leon-line)] rounded px-2 py-1.5">
                <Badge tone={m.status === 'Sent' ? 'green' : 'neutral'}>{m.status}</Badge>
                <span className="font-semibold truncate">{m.subject}</span>
                <span className="text-[var(--leon-black)]/40 truncate">{m.to || 'no address on file'}{m.cc ? ` · cc ${m.cc}` : ''}</span>
                {m.html && <button onClick={() => setOpenMail(m)} className="text-[var(--leon-brown)] font-semibold shrink-0">View</button>}
              </div>
            ))}
          </div>
        </Collapsible>
      )}
      <Modal wide open={!!openMail} onClose={() => setOpenMail(null)} title={openMail ? openMail.subject : ''}
        footer={<Button variant="ghost" onClick={() => setOpenMail(null)}>Close</Button>}>
        {openMail && (
          <>
            <p className="text-xs text-[var(--leon-black)]/50 mb-2">
              To <b>{openMail.to || '—'}</b>{openMail.cc ? <> · Cc <b>{openMail.cc}</b></> : null} · <b>queued, not sent</b>
            </p>
            <iframe title="Email" sandbox="" srcDoc={openMail.html} className="w-full h-[28rem] border border-[var(--leon-line)] rounded-lg bg-white" />
          </>
        )}
      </Modal>
    </div>
  );
}

// Per-person delivery rules. Lives in the profile because it is a personal
// setting, not an admin one — everyone tunes their own.
function NotificationPreferences({ ctx }) {
  const prefs = { ...makeNotificationPrefs(), ...((ctx.notificationPrefs || {})[ctx.currentUserId] || {}) };
  return (
    <div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">
        Which of these reach you, and how. <b>In-app</b> lands in your inbox immediately.
        <b> Email</b> queues a message &mdash; it will send once a mail service is connected.
      </p>
      <div className="flex items-end gap-3 mb-3 flex-wrap">
        <Field label="Send email to" className="!mb-0" hint="Blank uses your profile email">
          <TextInput value={prefs.emailAddress} onChange={e => ctx.setMyNotificationPrefs({ emailAddress: e.target.value })} placeholder={ctx.currentUser.email || 'you@leonintegra.com'} className="!w-64" />
        </Field>
        <Field label="Email frequency" className="!mb-0">
          <Select value={prefs.emailDigest} onChange={e => ctx.setMyNotificationPrefs({ emailDigest: e.target.value })} className="!w-40">
            <option value="immediate">Immediately</option>
            <option value="daily">Daily digest</option>
            <option value="off">No email at all</option>
          </Select>
        </Field>
      </div>
      <div className="border border-[var(--leon-line)] rounded-xl overflow-hidden">
        <table className="w-full text-sm">
          <thead>
            <tr className="bg-[var(--leon-cream)] text-left text-[11px] font-bold uppercase text-[var(--leon-black)]/50">
              <th className="px-3 py-2">Tell me when&hellip;</th>
              <th className="px-3 py-2 text-center w-20">In app</th>
              <th className="px-3 py-2 text-center w-20">Email</th>
            </tr>
          </thead>
          <tbody>
            {NOTIFICATION_GROUPS.map(group => {
              const evs = NOTIFICATION_EVENTS.filter(e => e.group === group);
              if (!evs.length) return null;
              return (
                <React.Fragment key={group}>
                  <tr><td colSpan={3} className="px-3 pt-3 pb-1 text-[11px] font-bold uppercase tracking-wide text-[var(--leon-brown)]">{group}</td></tr>
                  {evs.map(ev => (
                    <tr key={ev.key} className="border-t border-[var(--leon-line)]">
                      <td className="px-3 py-2">
                        <p className="font-semibold">{ev.label}</p>
                        <p className="text-[11px] text-[var(--leon-black)]/45">{ev.hint}</p>
                      </td>
                      {['inApp', 'email'].map(ch => (
                        <td key={ch} className="px-3 py-2 text-center">
                          <input type="checkbox" className="w-4 h-4 accent-[var(--leon-brown)]"
                            checked={wantsNotification(prefs, ev.key, ch)}
                            disabled={ch === 'email' && prefs.emailDigest === 'off'}
                            onChange={e => ctx.setNotificationChannel(ev.key, ch, e.target.checked)} />
                        </td>
                      ))}
                    </tr>
                  ))}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function NavBtn({ active, onClick, children, icon }) {
  return (
    <button onClick={onClick} className={`shrink-0 whitespace-nowrap px-1 py-1.5 rounded-md text-sm font-semibold transition ${active ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>
      {icon && <span aria-hidden="true" className={`mr-0.5 ${active ? '' : 'opacity-75'}`}>{icon}</span>}
      {children}
    </button>
  );
}
// A grouped nav entry. It highlights when any of its own destinations is the
// current view, so you can still tell where you are without opening it.
// A nav entry that also lists a hub's sections.
//   `onTrigger` — a daily destination must stay ONE click. Clicking the button
//     navigates to the hub AND drops the section list, so nothing became slower
//     by gaining a menu. Without it (the Library/Admin menus) the button only
//     opens the list, because those have no page of their own to land on.
//   `keepLabel` — a hub keeps its own name in the bar. Letting it rename itself
//     to the open section would mean the nav bar stopped saying "Accounts",
//     which is how you find it. The open section is still marked inside the
//     menu, which is where you look for it.
function NavMenu({ label, icon, items, view, open, onToggle, onTrigger, keepLabel }) {
  if (!items.length) return null;
  const activeItem = items.find(i => i.key === view);
  return (
    <div className="relative shrink-0" onClick={e => e.stopPropagation()}>
      <button onClick={() => { if (onTrigger) onTrigger(); onToggle(); }}
        className={`inline-flex items-center whitespace-nowrap px-1 py-1.5 rounded-md text-sm font-semibold transition ${activeItem ? 'bg-[var(--leon-black)] text-white' : open ? 'bg-[var(--leon-cream)] text-[var(--leon-black)]' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>
        <span aria-hidden="true" className={`mr-0.5 ${activeItem ? '' : 'opacity-75'}`}>{icon}</span>
        {keepLabel || !activeItem ? label : activeItem.label}
        <span aria-hidden="true" className="ml-0.5 text-[8px] opacity-50">&#9660;</span>
      </button>
      {open && (
        <div className="absolute left-0 top-full mt-1 z-40 min-w-[13rem] bg-white border border-[var(--leon-line)] rounded-lg shadow-lg py-1">
          {items.map(i => (
            <button key={i.key} onClick={() => { i.go(); onToggle(); }}
              className={`w-full text-left px-3 py-1.5 text-sm font-semibold flex items-center gap-2 ${view === i.key ? 'text-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'text-[var(--leon-black)]/70 hover:bg-[var(--leon-cream)]'}`}>
              <span aria-hidden="true" className="opacity-75">{i.icon}</span>{i.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}


// Self-service profile: every signed-in user can set their own photo and
// contact details — this only ever targets ctx.currentUserId, never another
// user's record (that stays Admin-only, in Users).
// targetUserId (Phase 10) — defaults to the logged-in person (the header's
// "Edit my profile" entry point); "Meet the Team" reuses this same modal to
// let an Admin (or the person themselves) edit someone else's rich profile,
// per the roadmap's "editable only by the person themselves or an Admin"
// rule. Self-service password change only ever applies to your own login,
// so that section is hidden when viewing someone else's profile.
const BIRTHDAY_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
function daysInBirthdayMonth(month) { return new Date(2000, month, 0).getDate(); }
function MyProfileModal({ open, onClose, ctx, targetUserId }) {
  const person = ctx.teamDirectory.find(p => p.id === (targetUserId || ctx.currentUserId)) || ctx.currentUser;
  const isSelf = person.id === ctx.currentUserId;
  const editable = isSelf || ctx.currentRole === 'Admin';
  const blank = { photoUrl: null, phone: '', mobile: '', title: '', birthday: '', bio: '', education: '', experience: '', aspirations: '', pictures: [], emailSignature: '', emailSignatureImage: null, emailSignatureImageName: '' };
  const [form, setForm] = useState(blank);
  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [pwError, setPwError] = useState('');
  useEffect(() => {
    if (open) {
      setForm({
        photoUrl: person.photoUrl, phone: person.phone || '', mobile: person.mobile || '', title: person.title || '', birthday: person.birthday || '',
        bio: person.bio || '', education: person.education || '', experience: person.experience || '', aspirations: person.aspirations || '',
        pictures: person.pictures || [], emailSignature: person.emailSignature || '',
        emailSignatureImage: person.emailSignatureImage || null,
        emailSignatureImageName: person.emailSignatureImageName || '',
      });
      setCurrentPassword(''); setNewPassword(''); setPwError('');
    }
  }, [open, person]);
  function submit() {
    ctx.updateUser(person.id, form);
    onClose();
  }
  const [pwBusy, setPwBusy] = useState(false);
  // The password lives at Supabase now, not on the person record. Writing to
  // `person.password` here would have looked like it worked and locked someone
  // out on their next sign-in, with the real password unchanged.
  async function changePassword() {
    if (!currentPassword) { setPwError('Enter your current password.'); return; }
    if (!newPassword) { setPwError('Enter a new password.'); return; }
    setPwBusy(true); setPwError('');
    try {
      const res = (typeof leonAuthChangePassword === 'function')
        ? await leonAuthChangePassword(person.email, currentPassword, newPassword)
        : { ok: false, error: 'Sign-in is unavailable — reload the page.' };
      if (!res.ok) { setPwError(res.error); return; }
      // Clear the stale local field so nothing anywhere still holds a password.
      if (person.password) ctx.updateUser(person.id, { password: '' });
      setCurrentPassword(''); setNewPassword('');
      setPwError('Password updated. It applies the next time you sign in, on any device.');
    } finally { setPwBusy(false); }
  }
  function addPicture(url) { setForm(f => ({ ...f, pictures: [...f.pictures, url] })); }
  function removePicture(i) { setForm(f => ({ ...f, pictures: f.pictures.filter((_, idx) => idx !== i) })); }
  const [showPhoto, setShowPhoto] = useState(false);
  return (
    <Modal open={open} onClose={onClose} wide title={isSelf ? 'My Profile' : `Profile — ${person.name}`} footer={editable ? <><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></> : <Button onClick={onClose}>Close</Button>}>
      <div className="space-y-3">
        <div className="flex items-center gap-3">
          {editable ? (
            <div>
              <ImagePicker url={form.photoUrl} onChange={url => setForm({ ...form, photoUrl: url })} size={64} shape="circle" />
              {form.photoUrl && <button type="button" onClick={() => setShowPhoto(true)} className="block text-[11px] text-[var(--leon-brown)] font-semibold mt-1 hover:underline">View Larger</button>}
            </div>
          ) : (
            <button type="button" onClick={() => person.photoUrl && setShowPhoto(true)} title={person.photoUrl ? 'View photo larger' : ''} className={person.photoUrl ? 'cursor-pointer' : ''}>
              <Avatar name={person.name} url={person.photoUrl} size={64} />
            </button>
          )}
          <div>
            <p className="text-sm font-bold">{person.name}</p>
            <p className="text-xs text-[var(--leon-black)]/50">{person.title ? `${person.title} · ` : ''}{person.securityRole}</p>
          </div>
        </div>
        {showPhoto && <AttachmentViewerModal name={`${person.name} — Photo`} url={editable ? form.photoUrl : person.photoUrl}
          onClose={() => setShowPhoto(false)} replaceLabel="Replace photo"
          onReplace={editable ? (dataUrl => setForm({ ...form, photoUrl: dataUrl })) : null} />}
        {editable ? (
          <>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Email" hint="Used to sign in — contact an Admin to change it"><TextInput value={person.email} disabled className="!bg-[var(--leon-cream)] !text-[var(--leon-black)]/60" /></Field>
              <Field label="Title"><TextInput value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} placeholder="e.g. Senior Project Coordinator" /></Field>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
              <Field label="Mobile"><TextInput value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} /></Field>
            </div>
            <Field label="Birthday" hint="Shown on everyone's Calendar every year — day and month only, no birth year">
              <div className="flex gap-2">
                <Select
                  value={form.birthday ? String(fromISO(form.birthday).getMonth() + 1) : ''}
                  onChange={e => {
                    const month = Number(e.target.value);
                    const day = Math.min(form.birthday ? fromISO(form.birthday).getDate() : 1, daysInBirthdayMonth(month));
                    setForm({ ...form, birthday: `2000-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` });
                  }}
                  className="!w-36"
                >
                  <option value="">Month</option>
                  {BIRTHDAY_MONTHS.map((m, i) => <option key={m} value={i + 1}>{m}</option>)}
                </Select>
                <Select
                  value={form.birthday ? String(fromISO(form.birthday).getDate()) : ''}
                  onChange={e => {
                    const month = form.birthday ? fromISO(form.birthday).getMonth() + 1 : 1;
                    setForm({ ...form, birthday: `2000-${String(month).padStart(2, '0')}-${String(Number(e.target.value)).padStart(2, '0')}` });
                  }}
                  disabled={!form.birthday}
                  className="!w-24"
                >
                  <option value="">Day</option>
                  {Array.from({ length: daysInBirthdayMonth(form.birthday ? fromISO(form.birthday).getMonth() + 1 : 1) }, (_, i) => i + 1).map(d => <option key={d} value={d}>{d}</option>)}
                </Select>
              </div>
            </Field>
            <Field label="Bio"><TextArea rows={2} value={form.bio} onChange={e => setForm({ ...form, bio: e.target.value })} /></Field>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Education"><TextArea rows={2} value={form.education} onChange={e => setForm({ ...form, education: e.target.value })} /></Field>
              <Field label="Experience"><TextArea rows={2} value={form.experience} onChange={e => setForm({ ...form, experience: e.target.value })} /></Field>
            </div>
            <Field label="Aspirations"><TextArea rows={2} value={form.aspirations} onChange={e => setForm({ ...form, aspirations: e.target.value })} /></Field>
            <div>
              <p className="text-xs font-semibold mb-1">Pictures</p>
              <div className="flex items-center gap-2 flex-wrap">
                {form.pictures.map((p, i) => (
                  <span key={i} className="relative">
                    <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                    <button type="button" onClick={() => removePicture(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
                  </span>
                ))}
                <ImagePicker url={null} onChange={addPicture} size={56} />
              </div>
            </div>
            {isSelf && (
              <div className="pt-2 border-t border-[var(--leon-line)]">
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">Change My Password</p>
                <div className="grid grid-cols-2 gap-3">
                  <Field label="Current Password"><TextInput type="password" value={currentPassword} onChange={e => setCurrentPassword(e.target.value)} /></Field>
                  <Field label="New Password"><TextInput type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} /></Field>
                </div>
                <div className="flex items-center gap-2 mt-1.5">
                  <Button size="sm" variant="outline" onClick={changePassword} disabled={pwBusy}>
                    {pwBusy ? 'Updating…' : 'Update Password'}
                  </Button>
                  {pwError && <span className="text-xs text-[var(--leon-black)]/60">{pwError}</span>}
                </div>
              </div>
            )}
            {isSelf && (
              <div className="pt-2 border-t border-[var(--leon-line)]">
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">My Email Signature</p>
                <EmailSignatureField form={form} setForm={setForm} person={person} />
              </div>
            )}
            {isSelf && (
              <div className="pt-2 border-t border-[var(--leon-line)]">
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">Signing in on this device</p>
                <BiometricUnlockField person={person} />
              </div>
            )}
            {/* Notification rules are a personal setting, so they live with the
                rest of what a person controls about their own account. */}
            {isSelf && (
              <div className="pt-2 border-t border-[var(--leon-line)]">
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">My Notifications</p>
                <NotificationPreferences ctx={ctx} />
              </div>
            )}
          </>
        ) : (
          <>
            {person.birthday && <p className="text-xs text-[var(--leon-black)]/60">🎂 {fromISO(person.birthday).toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}</p>}
            {person.bio && <p className="text-sm">{person.bio}</p>}
            {person.education && <p className="text-xs text-[var(--leon-black)]/60"><strong>Education:</strong> {person.education}</p>}
            {person.experience && <p className="text-xs text-[var(--leon-black)]/60"><strong>Experience:</strong> {person.experience}</p>}
            {person.aspirations && <p className="text-xs text-[var(--leon-black)]/60"><strong>Aspirations:</strong> {person.aspirations}</p>}
            {(person.pictures || []).length > 0 && (
              <div className="flex items-center gap-2 flex-wrap">
                {person.pictures.map((p, i) => <ClickableImage key={i} src={p} name={`${person.name} photo`} className="w-16 h-16 object-cover rounded-md" />)}
              </div>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}

// ============================================================================
// Dashboard
// ============================================================================
// ---- My To-Do (§ personal to-do request) — the login landing page. Pulls
// together three kinds of items that used to live in three disconnected
// places: personal Events/To-Dos (new, this request), assigned project
// tasks (existing, cross-project), and scope-stage deadlines (existing
// stages, now assignable to a specific person via assignStageUser). ----
const PERSONAL_ITEM_ICONS = { 'To-Do': '✅', Meeting: '👥', 'Virtual Meeting': '💻', Event: '🎉', Reminder: '⏰', Other: '📌', Task: '📋', Deadline: '🏁', 'Delivery Approval': '🚚', 'Jobsite Visit': '🏗️', 'Financial Issue': '💰', 'Installation Reschedule': '🔁', 'Installation Approval': '🛠️', 'Punch Verification': '📝', 'Stock Conversion': '📦' };
// These My-To-Do "kinds" are read-only surfacing of another record's own
// status (Phase 11 cross-cutting audit) — not a personal item with its own
// Open/Done toggle, so no checkbox.
const NON_CHECKABLE_TODO_KINDS = ['Deadline', 'Delivery Approval', 'Jobsite Visit', 'Financial Issue', 'Installation Reschedule', 'Installation Approval', 'Punch Verification', 'Stock Conversion'];
const MYTODO_VIEW_MODES = ['List', 'Calendar', 'Icons'];
// Everything currently on one person's plate, assembled from every module
// that can assign work. Extracted from MyToDoView so the calendar export
// (.ics) is built from exactly the same list the person sees, rather than a
// second definition of "my work" that can drift from it.
function collectMyItems(ctx, viewingId) {
  // A private item only ever surfaces for its owner or an invited attendee —
  // gated on who is actually logged in, not on whose queue is being peeked
  // at, so "Viewing" someone else's list never leaks their private items.
  const myPersonalItems = ctx.personalItems.filter(i =>
    (i.userId === viewingId || i.attendeeIds.includes(viewingId)) &&
    (!i.private || i.userId === ctx.currentUserId || i.attendeeIds.includes(ctx.currentUserId))
  );
  const myTasks = ctx.projects.flatMap(p => p.tasks.filter(t => t.assigneeId === viewingId).map(t => ({ ...t, projectId: p.id, projectName: p.name })));
  const myDeadlines = ctx.projects.flatMap(p => p.scopes.flatMap(sc => sc.stages.filter(st => st.assignedUserId === viewingId && st.status !== 'Completed').map(st => ({ ...st, projectId: p.id, projectName: p.name, scopeId: sc.id, scopeName: sc.name }))));
  // A delivery lands here for whoever it's assigned to for approval —
  // separate from the driver/helper's own Delivery Driver Hub, which shows
  // deliveries by driverId instead once they're scheduled.
  const myDeliveryApprovals = ctx.projects.flatMap(p => p.deliveries.filter(d => d.approverId === viewingId && d.approvalStatus === 'Pending Approval').map(d => ({ ...d, projectId: p.id, projectName: p.name })));
  const myJobsiteVisits = ctx.projects.flatMap(p => p.jobsiteVisits.filter(v => v.assigneeId === viewingId).map(v => ({ ...v, projectId: p.id, projectName: p.name })));
  // ---- Phase 11 cross-cutting audit — every approval gate built in Phases
  // 5-10 gets an explicit assignee and a My-To-Do source, same as the
  // existing Delivery Approval pattern above. Flows already gated purely by
  // role (Admin/Logistic Manager/PC — no single person ever "owns" the
  // approval) surface to every viewer who holds that role, since inventing
  // a single-assignee field would fight how those roles were deliberately
  // designed to work everywhere else in this app. ----
  const viewingPerson = ctx.teamDirectory.find(p => p.id === viewingId);
  const myFinancialIssues = ctx.projects.flatMap(p => (p.financialIssues || []).filter(i => i.status === 'Open' && i.assigneeIds.includes(viewingId)).map(i => ({ ...i, projectId: p.id, projectName: p.name })));
  // Installer reschedule requests go to the project's own Project
  // Coordinator; PC-scheduled installer approvals go to whichever internal
  // crew member it's assigned to (resolved by name — no id field exists on
  // assignedCrew — subcontractor-assigned records are already visible to
  // that subcontractor in their own portal, which reuses this same
  // InstallationTab, so they don't need a second surfacing here).
  const myInstallationReschedules = ctx.projects.flatMap(p => (p.installationRecords || []).filter(r => r.approvalStatus === 'Reschedule Requested' && teamMemberFor(p, 'Project Coordinator') === viewingId).map(r => ({ ...r, projectId: p.id, projectName: p.name })));
  const myInstallationApprovals = viewingPerson ? ctx.projects.flatMap(p => (p.installationRecords || []).filter(r => r.approvalStatus === 'Pending Installer Approval' && r.assignedCrew === viewingPerson.name).map(r => ({ ...r, projectId: p.id, projectName: p.name }))) : [];
  const myPunchVerifications = ctx.projects.flatMap(p => (p.punchItems || []).filter(pi => pi.status === 'Completed – Awaiting Verification' && teamMemberFor(p, 'Project Coordinator') === viewingId).map(pi => ({ ...pi, projectId: p.id, projectName: p.name })));
  const myStockConversionApprovals = viewingPerson ? ctx.materialAllocations.filter(a => a.stockConversionRequest && a.status !== 'Converted to Stock' &&
    ((viewingPerson.securityRole === 'Admin' && !a.stockConversionRequest.adminApproved) || (viewingPerson.securityRole === 'Logistic Manager' && !a.stockConversionRequest.logisticManagerApproved))
  ).map(a => ({ ...a, projectName: (ctx.projects.find(p => p.id === a.projectId) || {}).name })) : [];

  return [
    ...myPersonalItems.map(i => ({ kind: i.type, id: i.id, title: i.title, date: i.date, completed: i.status === 'Done', raw: i, projectName: (ctx.projects.find(p => p.id === i.projectId) || {}).name })),
    ...myTasks.map(t => ({ kind: 'Task', id: t.id, title: t.title, date: t.dueDate, completed: t.status === 'Completed', raw: t, projectName: t.projectName })),
    ...myDeadlines.map(st => ({ kind: 'Deadline', id: st.id, title: `${st.name} — ${st.scopeName}`, date: st.plannedDue, completed: false, raw: st, projectName: st.projectName })),
    ...myDeliveryApprovals.map(d => ({ kind: 'Delivery Approval', id: d.id, title: `${d.deliveryNumber} — ${d.description}`, date: d.date, completed: false, raw: d, projectName: d.projectName })),
    ...myJobsiteVisits.map(v => ({ kind: 'Jobsite Visit', id: v.id, title: v.purpose || 'Jobsite Visit', date: v.date, completed: false, raw: v, projectName: v.projectName })),
    ...myFinancialIssues.map(i => ({ kind: 'Financial Issue', id: i.id, title: i.description, date: i.createdDate, completed: false, raw: i, projectName: i.projectName })),
    ...myInstallationReschedules.map(r => ({ kind: 'Installation Reschedule', id: r.id, title: `Reschedule requested — ${[r.building, r.room].filter(Boolean).join(' · ') || r.item || 'Installation'}`, date: r.rescheduleRequest ? r.rescheduleRequest.requestedDate : null, completed: false, raw: r, projectName: r.projectName })),
    ...myInstallationApprovals.map(r => ({ kind: 'Installation Approval', id: r.id, title: `Approve schedule — ${[r.building, r.room].filter(Boolean).join(' · ') || r.item || 'Installation'}`, date: r.scheduledStart, completed: false, raw: r, projectName: r.projectName })),
    ...myPunchVerifications.map(pi => ({ kind: 'Punch Verification', id: pi.id, title: `Verify — ${pi.item || 'Punch Item'}`, date: pi.completionDate, completed: false, raw: pi, projectName: pi.projectName })),
    ...myStockConversionApprovals.map(a => ({ kind: 'Stock Conversion', id: a.id, title: `Approve stock conversion — value ${fmtMoney(a.stockConversionRequest.value)}`, date: a.stockConversionRequest.requestedDate, completed: false, raw: a, projectName: a.projectName })),
  ];
}

function MyToDoView({ ctx }) {
  const [viewingId, setViewingId] = useState(ctx.currentUserId);
  const [showAdd, setShowAdd] = useState(false);
  const [viewMode, setViewMode] = useState('List');
  const [detailFor, setDetailFor] = useState(null);
  const [editingItem, setEditingItem] = useState(null);
  const isSelf = viewingId === ctx.currentUserId;

  const items = collectMyItems(ctx, viewingId);
  const buckets = { Overdue: [], Today: [], Upcoming: [], Completed: [] };
  items.forEach(it => buckets[dateBucket(it.date, it.completed)].push(it));

  function toggleItem(it) {
    if (it.kind === 'Task') ctx.setTaskStatus(it.raw.projectId, it.id, it.completed ? 'Open' : 'Completed');
    else ctx.setPersonalItemStatus(it.id, it.completed ? 'Open' : 'Done');
  }
  function openItem(it) {
    if (it.kind === 'Task') ctx.goProjectTab(it.raw.projectId, 'tasks');
    else if (it.kind === 'Deadline') ctx.goProjectTab(it.raw.projectId, 'scopes');
    else if (it.kind === 'Delivery Approval') ctx.goProjectTab(it.raw.projectId, 'delivery');
    else if (it.kind === 'Jobsite Visit') ctx.goProjectTab(it.raw.projectId, 'overview');
    else if (it.kind === 'Financial Issue') ctx.goProjectTab(it.raw.projectId, 'financials', 'issues');
    else if (it.kind === 'Installation Reschedule' || it.kind === 'Installation Approval' || it.kind === 'Punch Verification') ctx.goProjectTab(it.raw.projectId, 'installation');
    else if (it.kind === 'Stock Conversion') ctx.goWarehouse();
    else setDetailFor(it);
  }

  function ItemRow({ it }) {
    return (
      <div className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
        <div className="flex items-center gap-2 min-w-0">
          {!NON_CHECKABLE_TODO_KINDS.includes(it.kind) && <input type="checkbox" checked={it.completed} disabled={!isSelf} onChange={() => toggleItem(it)} />}
          <div className="min-w-0 cursor-pointer" onClick={() => openItem(it)}>
            <p className="text-sm font-semibold truncate hover:underline">{PERSONAL_ITEM_ICONS[it.kind] || ''} {it.title}{it.raw.private && ' 🔒'}</p>
            <p className="text-xs text-[var(--leon-black)]/50">{it.date ? fmtDate(it.date) : 'No date'}{it.raw.time ? ` · ${fmtTimeRange(it.raw.time, it.raw.durationMinutes)}` : ''}{it.raw.location ? ` · ${it.raw.location}` : ''}{it.projectName ? ` · ${it.projectName}` : ''}</p>
          </div>
        </div>
        <div className="flex items-center gap-2 shrink-0">
          {it.raw.priority && <Badge tone={it.raw.priority === 'High' ? 'red' : it.raw.priority === 'Medium' ? 'yellow' : 'neutral'}>{it.raw.priority}</Badge>}
          <Badge tone="neutral">{it.kind}</Badge>
          {isSelf && it.raw.userId === ctx.currentUserId && PERSONAL_ITEM_TYPES.includes(it.kind) && <IconBtn title="Remove" onClick={() => ctx.removePersonalItem(it.id)}>✕</IconBtn>}
        </div>
      </div>
    );
  }

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-5">
        <div>
          <h1 className="text-2xl font-bold">My To-Do</h1>
          <p className="text-sm text-[var(--leon-black)]/50">Personal items, assigned tasks, and scope deadlines — all in one place.</p>
        </div>
        <div className="flex items-center gap-2 flex-wrap">
          <Field label="Viewing">
            <Select value={viewingId} onChange={e => setViewingId(e.target.value)} className="!w-56">
              <option value={ctx.currentUserId}>Me ({ctx.currentUserName})</option>
              {ctx.teamDirectory.filter(p => p.id !== ctx.currentUserId && p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white self-end">
            {MYTODO_VIEW_MODES.map(v => (
              <button key={v} onClick={() => setViewMode(v)} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors ${viewMode === v ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>{v}</button>
            ))}
          </div>
          {/* One-way snapshot for Outlook/Google/Apple Calendar — built from
              this exact list, so it can't drift from what you see here. */}
          <Button variant="outline" onClick={() => downloadIcs(
            `leon-${(ctx.teamDirectory.find(p => p.id === viewingId) || {}).name || 'my'}-work-${todayISO()}`.replace(/\s+/g, '-').toLowerCase(),
            buildIcsForItems(items, (ctx.teamDirectory.find(p => p.id === viewingId) || {}).name))}>
            &#128197; Export to calendar
          </Button>
          {isSelf && <Button onClick={() => setShowAdd(true)}>+ Add Item</Button>}
        </div>
      </div>

      {viewMode === 'List' && ['Overdue', 'Today', 'Upcoming', 'Completed'].map(bucket => (
        <Collapsible key={bucket} title={bucket} count={buckets[bucket].length}>
          {buckets[bucket].length === 0 ? <EmptyState text="Nothing here." /> : (
            <div className="space-y-1.5">{buckets[bucket].map(it => <ItemRow key={it.kind + it.id} it={it} />)}</div>
          )}
        </Collapsible>
      ))}

      {viewMode === 'Icons' && (
        items.length === 0 ? <EmptyState text="Nothing here." /> : (
          <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
            {items.map(it => (
              <div key={it.kind + it.id} className="border border-[var(--leon-line)] rounded-xl p-4 bg-white cursor-pointer hover:border-[var(--leon-brown)]" onClick={() => openItem(it)}>
                <div className="text-3xl mb-2">{PERSONAL_ITEM_ICONS[it.kind] || '📌'}</div>
                <p className="text-sm font-bold truncate">{it.title}{it.raw.private && ' 🔒'}</p>
                <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{it.date ? fmtDate(it.date) : 'No date'}{it.raw.time ? ` · ${fmtTimeRange(it.raw.time, it.raw.durationMinutes)}` : ''}</p>
                <div className="flex items-center gap-1.5 mt-2">
                  <Badge tone="neutral">{it.kind}</Badge>
                  {it.completed && <Badge tone="green">Done</Badge>}
                </div>
              </div>
            ))}
          </div>
        )
      )}

      {viewMode === 'Calendar' && <MyToDoCalendar items={items} onOpen={openItem} />}

      <PersonalItemFormModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
      <PersonalItemFormModal open={!!editingItem} item={editingItem} onClose={() => setEditingItem(null)} ctx={ctx} />
      <PersonalItemDetailModal open={!!detailFor} item={detailFor?.raw} onClose={() => setDetailFor(null)}
        onEdit={detailFor?.raw.userId === ctx.currentUserId ? () => { setEditingItem(detailFor.raw); setDetailFor(null); } : null}
        ctx={ctx} />
    </div>
  );
}
// Mirrors CalendarView's Day/Week/Month/Agenda pattern exactly (CALENDAR_VIEWS,
// shiftCalendarDate, startOfWeek, startOfMonthGrid — all shared globals) but
// driven by this user's own items instead of project-wide events.
function MyToDoCalendar({ items, onOpen }) {
  const [selectedDate, setSelectedDate] = useState(todayISO());
  const [view, setView] = useState('Month');
  const byDate = {};
  items.forEach(it => { if (it.date) (byDate[it.date] = byDate[it.date] || []).push(it); });
  // Timed items first (ascending), untimed items after — applied once here so
  // Day, Week, and Agenda all inherit it since they all read from byDate.
  Object.values(byDate).forEach(list => list.sort((a, b) => {
    const ta = a.raw.time, tb = b.raw.time;
    if (!ta && !tb) return 0;
    if (!ta) return 1;
    if (!tb) return -1;
    return ta < tb ? -1 : ta > tb ? 1 : 0;
  }));
  const dayItems = (byDate[selectedDate] || []);
  const itemDates = new Set(Object.keys(byDate));

  function shift(dir) { setSelectedDate(d => shiftCalendarDate(d, view, dir)); }

  function MyToDoDayList({ list }) {
    if (list.length === 0) return <EmptyState text="Nothing scheduled for this day." />;
    return (
      <div className="space-y-1.5">
        {list.map(it => (
          <div key={it.kind + it.id} onClick={() => onOpen(it)} className="flex items-center gap-3 border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-white cursor-pointer hover:bg-[var(--leon-cream)]">
            <Badge tone="neutral">{it.kind}</Badge>
            <div className="min-w-0">
              <p className="text-sm font-semibold truncate">{PERSONAL_ITEM_ICONS[it.kind] || ''} {it.title}{it.raw.private && ' 🔒'}</p>
              <p className="text-xs text-[var(--leon-black)]/50">{fmtTimeRange(it.raw.time, it.raw.durationMinutes)}{it.raw.location ? ` · ${it.raw.location}` : ''}{it.projectName ? ` · ${it.projectName}` : ''}</p>
            </div>
          </div>
        ))}
      </div>
    );
  }

  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-4 flex-wrap">
        <div className="flex items-center gap-2">
          <Button size="sm" variant="ghost" onClick={() => shift(-1)}>← Prev</Button>
          <TextInput type="date" value={selectedDate} onChange={e => setSelectedDate(e.target.value)} className="!w-44" />
          <Button size="sm" variant="ghost" onClick={() => shift(1)}>Next →</Button>
          <Button size="sm" variant="ghost" onClick={() => setSelectedDate(todayISO())}>Today</Button>
        </div>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {CALENDAR_VIEWS.map(v => (
            <button key={v} onClick={() => setView(v)} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors ${view === v ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>{v}</button>
          ))}
        </div>
      </div>

      {view === 'Day' && (
        <>
          <div className="flex gap-1 mb-5 overflow-x-auto pb-1">
            {Array.from({ length: 14 }, (_, i) => addDays(selectedDate, i - 3)).map(d => (
              <button key={d} onClick={() => setSelectedDate(d)} className={`shrink-0 w-14 rounded-lg border px-1 py-2 text-center ${d === selectedDate ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'}`}>
                <div className="text-[10px] uppercase opacity-60">{fromISO(d).toLocaleDateString('en-US', { weekday: 'short' })}</div>
                <div className="text-sm font-bold">{fromISO(d).getDate()}</div>
                {itemDates.has(d) && <div className={`w-1 h-1 rounded-full mx-auto mt-1 ${d === selectedDate ? 'bg-white' : 'bg-[var(--leon-brown)]'}`} />}
              </button>
            ))}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayItems.length} item{dayItems.length === 1 ? '' : 's'}</h2>
          <MyToDoDayList list={dayItems} />
        </>
      )}

      {view === 'Week' && (
        <>
          <div className="grid grid-cols-7 gap-1.5 mb-5">
            {Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(selectedDate), i)).map(d => {
              const dItems = byDate[d] || [];
              const isToday = d === todayISO();
              return (
                <button key={d} onClick={() => setSelectedDate(d)} className={`text-left rounded-lg border px-2 py-2 min-h-[110px] align-top ${d === selectedDate ? 'border-[var(--leon-black)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'}`}>
                  <div className="flex items-center justify-between">
                    <span className="text-[10px] uppercase opacity-60">{fromISO(d).toLocaleDateString('en-US', { weekday: 'short' })}</span>
                    <span className={`text-xs font-bold ${isToday ? 'text-[var(--leon-brown)]' : ''}`}>{fromISO(d).getDate()}</span>
                  </div>
                  <div className="mt-1.5 space-y-1">
                    {dItems.slice(0, 4).map(it => <div key={it.kind + it.id} className="text-[10px] truncate px-1 py-0.5 rounded bg-[var(--leon-line)]">{it.title}</div>)}
                    {dItems.length > 4 && <div className="text-[10px] text-[var(--leon-black)]/40">+{dItems.length - 4} more</div>}
                  </div>
                </button>
              );
            })}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayItems.length} item{dayItems.length === 1 ? '' : 's'}</h2>
          <MyToDoDayList list={dayItems} />
        </>
      )}

      {view === 'Month' && (
        <>
          <h2 className="font-bold text-sm mb-2">{fromISO(selectedDate).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}</h2>
          <div className="grid grid-cols-7 gap-1.5 mb-5">
            {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => <div key={d} className="text-[10px] uppercase font-semibold text-[var(--leon-black)]/40 text-center pb-1">{d}</div>)}
            {Array.from({ length: 42 }, (_, i) => addDays(startOfMonthGrid(selectedDate), i)).map(d => {
              const dItems = byDate[d] || [];
              const inMonth = fromISO(d).getMonth() === fromISO(selectedDate).getMonth();
              const isToday = d === todayISO();
              return (
                <button key={d} onClick={() => setSelectedDate(d)} className={`text-left rounded-lg border px-1.5 py-1.5 min-h-[68px] align-top ${d === selectedDate ? 'border-[var(--leon-black)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'} ${inMonth ? '' : 'opacity-35'}`}>
                  <span className={`text-xs font-bold ${isToday ? 'text-[var(--leon-brown)]' : ''}`}>{fromISO(d).getDate()}</span>
                  <div className="mt-1 flex flex-wrap gap-0.5">
                    {dItems.slice(0, 3).map(it => <span key={it.kind + it.id} className={`w-1.5 h-1.5 rounded-full ${d === selectedDate ? 'bg-[var(--leon-brown)]' : 'bg-[var(--leon-line)] border border-[var(--leon-brown)]/40'}`} />)}
                    {dItems.length > 3 && <span className="text-[9px] text-[var(--leon-black)]/40">+{dItems.length - 3}</span>}
                  </div>
                </button>
              );
            })}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayItems.length} item{dayItems.length === 1 ? '' : 's'}</h2>
          <MyToDoDayList list={dayItems} />
        </>
      )}

      {view === 'Agenda' && (
        <div className="space-y-4">
          {Object.keys(byDate).sort().filter(d => d >= todayISO()).length === 0 ? (
            <EmptyState text="No upcoming items." />
          ) : (
            Object.keys(byDate).sort().filter(d => d >= todayISO()).slice(0, 60).map(d => (
              <div key={d}>
                <h3 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">{fmtDate(d)}{d === todayISO() ? ' — Today' : ''}</h3>
                <MyToDoDayList list={byDate[d]} />
              </div>
            ))
          )}
        </div>
      )}
    </div>
  );
}
function PersonalItemDetailModal({ open, item, onClose, onEdit, ctx }) {
  if (!item) return null;
  const project = item.projectId ? ctx.projects.find(p => p.id === item.projectId) : null;
  const scope = project && item.scopeId ? project.scopes.find(s => s.id === item.scopeId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`${item.type} — ${item.title}`} printable
      fields={[
        { label: 'Type', value: item.type }, { label: 'Status', value: item.status }, { label: 'Priority', value: item.priority },
        { label: 'Date', value: item.date ? fmtDate(item.date) : '—' }, { label: 'Time', value: fmtTimeRange(item.time, item.durationMinutes) || '—' }, { label: 'Location', value: item.location },
        { label: 'Project', value: project ? project.name : '—' }, { label: 'Scope', value: scope ? scope.name : '—' },
        { label: 'Attendees', value: item.attendeeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') || '—' },
        { label: 'Repeat', value: item.repeat }, { label: 'Private', value: item.private ? 'Yes' : 'No' },
        { label: 'Notes', value: item.notes }, { label: 'Created By', value: item.createdBy }, { label: 'Created Date', value: fmtDate(item.createdDate) },
      ]}
      attachments={item.attachments}
    >
      {onEdit && <div className="no-print"><Button size="sm" variant="outline" onClick={onEdit}>Edit</Button></div>}
    </RecordDetailModal>
  );
}
// Handles both Add (item=null) and Edit (item set) — same fields either way;
// Repeat/Occurrences only make sense at creation time (repeat materializes
// discrete records up front, see addPersonalItem) so it's hidden when editing.
function PersonalItemFormModal({ open, item, onClose, ctx }) {
  const isEdit = !!item;
  const blank = { type: 'To-Do', title: '', date: todayISO(), time: '', durationMinutes: null, location: '', projectId: '', scopeId: '', attendeeIds: [], repeat: 'None', occurrences: 4, attachments: [], private: false, notes: '', priority: 'Medium' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(item ? { ...blank, ...item, projectId: item.projectId || '', scopeId: item.scopeId || '' } : blank); }, [open, item]);
  function submit() {
    if (!form.title.trim()) return;
    if (isEdit) ctx.updatePersonalItem(item.id, { ...form, projectId: form.projectId || null, scopeId: form.scopeId || null });
    else ctx.addPersonalItem({ ...form, projectId: form.projectId || null, scopeId: form.scopeId || null });
    onClose();
  }
  function toggleAttendee(id) { setForm(f => ({ ...f, attendeeIds: f.attendeeIds.includes(id) ? f.attendeeIds.filter(x => x !== id) : [...f.attendeeIds, id] })); }
  function addAttachment(fname, url) { setForm(f => ({ ...f, attachments: [...f.attachments, { id: uid('att'), name: fname, url, uploadedBy: ctx.currentUserName, uploadedDate: todayISO() }] })); }
  function removeAttachment(id) { setForm(f => ({ ...f, attachments: f.attachments.filter(a => a.id !== id) })); }
  const project = ctx.projects.find(p => p.id === form.projectId);
  return (
    <Modal open={open} onClose={onClose} wide title={isEdit ? 'Edit To-Do / Event' : 'Add To-Do / Event'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Title"><TextInput value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} /></Field>
          <Field label="Type"><Select value={form.type} onChange={e => setForm({ ...form, type: e.target.value })}>{PERSONAL_ITEM_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.time} onChange={e => setForm({ ...form, time: e.target.value, durationMinutes: e.target.value ? form.durationMinutes : null })} /></Field>
          <Field label="Duration">
            <Select value={form.durationMinutes || ''} onChange={e => setForm({ ...form, durationMinutes: e.target.value ? Number(e.target.value) : null })} disabled={!form.time}>
              <option value="">—</option>
              {PERSONAL_ITEM_DURATIONS.map(m => <option key={m} value={m}>{durationLabel(m)}</option>)}
            </Select>
          </Field>
          <Field label="Urgency"><Select value={form.priority} onChange={e => setForm({ ...form, priority: e.target.value })}>{PERSONAL_ITEM_PRIORITIES.map(p => <option key={p}>{p}</option>)}</Select></Field>
        </div>
        <Field label="Location" hint="Address, room, or a video call link"><TextInput value={form.location} onChange={e => setForm({ ...form, location: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Associate with Project (optional)">
            <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>
              <option value="">— none —</option>
              {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <Field label="Scope (optional)">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })} disabled={!project}>
              <option value="">— none —</option>
              {(project ? project.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Additional People">
          <div className="flex flex-wrap gap-1.5 border border-[var(--leon-line)] rounded-lg p-2 max-h-28 overflow-y-auto">
            {ctx.teamDirectory.filter(p => p.id !== ctx.currentUserId && p.active).map(p => (
              <label key={p.id} className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border cursor-pointer ${form.attendeeIds.includes(p.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
                <input type="checkbox" className="hidden" checked={form.attendeeIds.includes(p.id)} onChange={() => toggleAttendee(p.id)} /> {p.name}
              </label>
            ))}
          </div>
        </Field>
        {!isEdit && (
          <div className="grid grid-cols-2 gap-3 items-end">
            <Field label="Repeat"><Select value={form.repeat} onChange={e => setForm({ ...form, repeat: e.target.value })}>{PERSONAL_ITEM_REPEAT_OPTIONS.map(r => <option key={r}>{r}</option>)}</Select></Field>
            {form.repeat !== 'None' && <Field label="Occurrences"><TextInput type="number" min="2" value={form.occurrences} onChange={e => setForm({ ...form, occurrences: e.target.value })} /></Field>}
          </div>
        )}
        <Field label="Attachments">
          <div className="space-y-1.5">
            {form.attachments.map(a => (
              <div key={a.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded-md px-2 py-1">
                <span>{a.name}</span>
                <IconBtn title="Remove" onClick={() => removeAttachment(a.id)}>✕</IconBtn>
              </div>
            ))}
            <FileField name="" url={null} onChange={addAttachment} editable placeholder="+ Add attachment" />
          </div>
        </Field>
        <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={form.private} onChange={e => setForm({ ...form, private: e.target.checked })} /> Private (only visible to me and anyone I add above)</label>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---------------------------------------------------------------------------
// Dashboard blocks
// ---------------------------------------------------------------------------
// Each block is a small, self-contained read of data that already exists — none
// of them store anything. Which blocks a person sees is their own preference
// (`person.dashboardBlocks`), filtered through what their role is allowed to
// see, so a saved preference can never resurrect a block a permission change
// has since taken away.
function dashboardChosenBlocks(ctx) {
  const me = ctx.currentUser || {};
  const saved = Array.isArray(me.dashboardBlocks) ? me.dashboardBlocks : null;
  const keys = saved || dashboardBlocksFor(ctx.currentRole);
  return DASHBOARD_BLOCKS.filter(b => keys.indexOf(b.key) >= 0 && dashboardBlockAllowed(b, ctx))
    // keep the person's own order, not the registry's
    .sort((a, b) => keys.indexOf(a.key) - keys.indexOf(b.key));
}

function DashboardBlock({ block, children, right }) {
  return (
    <div className={`bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden ${block.span === 2 ? 'lg:col-span-2' : ''}`}>
      <div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--leon-line)] bg-[var(--leon-cream)]">
        <span aria-hidden="true">{block.icon}</span>
        <span className="text-[12px] font-bold uppercase tracking-wider text-[var(--leon-black)]/65">{block.label}</span>
        <span className="ml-auto">{right}</span>
      </div>
      <div className="p-4">{children}</div>
    </div>
  );
}

function DashMiniList({ rows, empty, onMore }) {
  if (!rows.length) return <p className="text-sm text-[var(--leon-black)]/45">{empty}</p>;
  return (
    <div className="space-y-1.5">
      {rows.slice(0, 6).map((r, i) => (
        <div key={r.key || i} className="flex items-baseline gap-2 text-sm">
          {r.tone && <span className={`shrink-0 w-1.5 h-1.5 rounded-full mt-1.5 ${r.tone}`} />}
          <span className="min-w-0 flex-1 truncate">{r.label}</span>
          {r.value !== undefined && <span className="shrink-0 text-[12px] tabular-nums text-[var(--leon-black)]/55">{r.value}</span>}
        </div>
      ))}
      {rows.length > 6 && (
        <button onClick={onMore} className="text-[12px] font-semibold text-[var(--leon-brown)] hover:underline pt-1">
          {rows.length - 6} more…
        </button>
      )}
    </div>
  );
}

function DashboardBlockBody({ blockKey, ctx }) {
  const today = todayISO();
  if (blockKey === 'myWork' || blockKey === 'dueAlerts') {
    const items = collectMyItems(ctx, ctx.currentUserId) || [];
    const open = items.filter(i => !i.completed);
    if (blockKey === 'myWork') {
      const rows = open
        .slice()
        .sort((a, b) => String(a.date || '9999').localeCompare(String(b.date || '9999')))
        .map(i => ({ key: i.id, label: `${i.title}${i.projectName ? ' · ' + i.projectName : ''}`, value: i.date ? fmtDate(i.date) : '' }));
      return <><div className="text-2xl font-bold tabular-nums mb-2">{open.length}</div>
        <DashMiniList rows={rows} empty="Nothing assigned to you is open." onMore={ctx.goCalendar} /></>;
    }
    const alerts = buildDueAlerts(open, today) || [];
    const rows = alerts.map(a => ({
      key: a.id, label: a.title, value: a.date ? fmtDate(a.date) : '',
      tone: a.severity === 'overdue' ? 'bg-[var(--leon-red)]' : 'bg-amber-400',
    }));
    return <><div className="text-2xl font-bold tabular-nums mb-2">{alerts.length}</div>
      <DashMiniList rows={rows} empty="Nothing overdue or due this week." onMore={ctx.goCalendar} /></>;
  }

  if (blockKey === 'pipeline') {
    const projects = ctx.deptProjects(ctx.projects || []);
    const sum = salesValueSummary(projects);
    const n = st => projects.filter(p => p.pipelineStatus === st).length;
    return (
      <div className="space-y-2">
        <div className="grid grid-cols-3 gap-2 text-center">
          {[['Leads', n('Lead')], ['Quotes out', n('Active Quotation')], ['Active', n('Active Job')]].map(([l, v]) => (
            <div key={l}><div className="text-xl font-bold tabular-nums">{v}</div>
              <div className="text-[10px] uppercase tracking-wider text-[var(--leon-black)]/50">{l}</div></div>
          ))}
        </div>
        <div className="text-sm"><span className="text-[var(--leon-black)]/50">Quotations out</span>{' '}
          <span className="font-semibold tabular-nums">{fmtMoney(sum.activeQuotationValue)}</span></div>
        <div className="text-sm"><span className="text-[var(--leon-black)]/50">Win rate</span>{' '}
          <span className="font-semibold tabular-nums">{sum.winRate === null ? '—' : sum.winRate.toFixed(0) + '%'}</span></div>
        <button onClick={ctx.goSales} className="text-[12px] font-semibold text-[var(--leon-brown)] hover:underline">Open Sales</button>
      </div>
    );
  }

  if (blockKey === 'quoteChase') {
    const q = buildQuoteChaseQueue(ctx.deptProjects(ctx.projects || []), ctx.accounts || [], today) || [];
    const due = q.filter(r => r.due);
    const rows = due.map(r => ({ key: r.projectId, label: r.projectName, value: r.roundLabel || '' }));
    return <><div className="text-2xl font-bold tabular-nums mb-2">{due.length}</div>
      <DashMiniList rows={rows} empty="No quotations need chasing this week." onMore={ctx.goCalendar} /></>;
  }

  if (blockKey === 'cash') {
    const from = today.slice(0, 8) + '01';
    const d = new Date(from); d.setMonth(d.getMonth() + 1);
    const to = d.toISOString().slice(0, 10);
    const ev = buildCashEvents(ctx.deptProjects(ctx.projects || []), ctx.cashEntries || [], from, to, ctx.creditCards || []) || [];
    const inn = ev.filter(e => e.direction === 'in').reduce((n, e) => n + (e.amount || 0), 0);
    const out = ev.filter(e => e.direction === 'out').reduce((n, e) => n + (e.amount || 0), 0);
    return (
      <div className="space-y-1.5">
        <div className="flex justify-between text-sm"><span className="text-[var(--leon-black)]/55">In</span>
          <span className="font-semibold tabular-nums text-[var(--leon-green,#1F7A3D)]">{fmtMoney(inn)}</span></div>
        <div className="flex justify-between text-sm"><span className="text-[var(--leon-black)]/55">Out</span>
          <span className="font-semibold tabular-nums text-[var(--leon-red)]">{fmtMoney(out)}</span></div>
        <div className="flex justify-between text-sm border-t border-[var(--leon-line)] pt-1.5">
          <span className="font-semibold">Net</span><span className="font-bold tabular-nums">{fmtMoney(inn - out)}</span></div>
        <button onClick={ctx.goAccounting} className="text-[12px] font-semibold text-[var(--leon-brown)] hover:underline">Open Accounting</button>
      </div>
    );
  }

  if (blockKey === 'jobProgress') {
    const rows = (buildJobProgressRows(ctx.deptProjects(ctx.projects || []), ctx.accounts || [], 'production') || [])
      .map(r => ({ key: r.scopeId, label: `${r.projectName} · ${r.scopeName}`, value: (r.pct || 0).toFixed(0) + '%' }));
    return <DashMiniList rows={rows} empty="No scopes in production." onMore={ctx.goAccounting} />;
  }

  if (blockKey === 'deliveries') {
    const rows = [];
    ctx.deptProjects(ctx.projects || []).forEach(p => (p.deliveries || []).forEach(d => {
      if (d.status === 'Delivered' || !d.scheduledDate) return;
      rows.push({ key: d.id, label: `${p.name} · ${d.destinationType || 'Delivery'}`, value: fmtDate(d.scheduledDate),
                  tone: d.scheduledDate < today ? 'bg-[var(--leon-red)]' : 'bg-amber-400' });
    }));
    rows.sort((a, b) => String(a.value).localeCompare(String(b.value)));
    return <><div className="text-2xl font-bold tabular-nums mb-2">{rows.length}</div>
      <DashMiniList rows={rows} empty="Nothing booked to arrive." onMore={ctx.goLogistics} /></>;
  }

  if (blockKey === 'issues') {
    const rows = [];
    ctx.deptProjects(ctx.projects || []).forEach(p => (p.issues || []).forEach(i => {
      if (i.status === 'Resolved' || i.status === 'Closed') return;
      rows.push({ key: i.id, label: `${p.name} · ${i.title || i.description || 'Issue'}`, value: i.status || '' });
    }));
    return <><div className="text-2xl font-bold tabular-nums mb-2">{rows.length}</div>
      <DashMiniList rows={rows} empty="No open issues." onMore={ctx.goDashboard} /></>;
  }

  if (blockKey === 'inbox') {
    const mine = (ctx.notifications || []).filter(n => (n.toUserIds || []).indexOf(ctx.currentUserId) >= 0);
    const rows = mine.slice(0, 12).map(n => ({ key: n.id, label: n.title, value: n.date ? fmtDate(n.date) : '' }));
    return <DashMiniList rows={rows} empty="Nothing new." onMore={ctx.goInbox || ctx.goCalendar} />;
  }
  return null;
}

// The picker. Only blocks this role is allowed to see are offered, so nobody is
// shown a permission they do not have and then denied it.
function DashboardCustomizeModal({ open, onClose, ctx }) {
  const me = ctx.currentUser || {};
  const allowed = DASHBOARD_BLOCKS.filter(b => dashboardBlockAllowed(b, ctx));
  const current = Array.isArray(me.dashboardBlocks) ? me.dashboardBlocks : dashboardBlocksFor(ctx.currentRole);
  const [picked, setPicked] = useState(current);
  useEffect(() => { if (open) setPicked(Array.isArray(me.dashboardBlocks) ? me.dashboardBlocks : dashboardBlocksFor(ctx.currentRole)); }, [open]);

  function toggle(key) {
    setPicked(p => p.indexOf(key) >= 0 ? p.filter(k => k !== key) : p.concat([key]));
  }
  function move(key, dir) {
    setPicked(p => {
      const i = p.indexOf(key), j = i + dir;
      if (i < 0 || j < 0 || j >= p.length) return p;
      const n = p.slice(); n[i] = p[j]; n[j] = p[i]; return n;
    });
  }
  function save() { ctx.updateUser(me.id, { dashboardBlocks: picked }); onClose(); }
  function reset() { ctx.updateUser(me.id, { dashboardBlocks: null }); onClose(); }

  return (
    <Modal open={open} onClose={onClose} title="Choose what your dashboard shows">
      <p className="text-sm text-[var(--leon-black)]/55 mb-3">
        Your own choice, saved against your account — it does not change anyone else's. Only what your
        role can see is offered here.
      </p>
      <div className="space-y-1.5 mb-4">
        {picked.map((k, idx) => {
          const b = allowed.find(x => x.key === k);
          if (!b) return null;
          return (
            <div key={k} className="flex items-center gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
              <input type="checkbox" checked readOnly onClick={() => toggle(k)} />
              <span aria-hidden="true">{b.icon}</span>
              <span className="text-sm font-semibold flex-1">{b.label}</span>
              <button onClick={() => move(k, -1)} disabled={idx === 0} className="px-1.5 disabled:opacity-25" title="Move up">↑</button>
              <button onClick={() => move(k, 1)} disabled={idx === picked.length - 1} className="px-1.5 disabled:opacity-25" title="Move down">↓</button>
            </div>
          );
        })}
        {!picked.length && <p className="text-sm text-[var(--leon-black)]/45">Nothing chosen — pick something below.</p>}
      </div>
      <div className="text-[11px] font-bold uppercase tracking-wider text-[var(--leon-black)]/50 mb-1.5">Available</div>
      <div className="space-y-1.5">
        {allowed.filter(b => picked.indexOf(b.key) < 0).map(b => (
          <button key={b.key} onClick={() => toggle(b.key)}
            className="w-full flex items-start gap-2 border border-dashed border-[var(--leon-line)] rounded-lg px-3 py-2 text-left hover:border-[var(--leon-brown)]">
            <span aria-hidden="true">{b.icon}</span>
            <span className="min-w-0">
              <span className="block text-sm font-semibold">{b.label}</span>
              <span className="block text-[11px] text-[var(--leon-black)]/50">{b.blurb}</span>
            </span>
            <span className="ml-auto text-[var(--leon-brown)] font-bold">+</span>
          </button>
        ))}
      </div>
      <div className="flex justify-between gap-2 mt-4">
        <Button variant="ghost" onClick={reset}>Back to my role's default</Button>
        <div className="flex gap-2"><Button variant="outline" onClick={onClose}>Cancel</Button><Button onClick={save}>Save</Button></div>
      </div>
    </Modal>
  );
}

function Dashboard({ ctx, filter, setFilter, search, setSearch }) {
  const [showNew, setShowNew] = useState(false);
  const [customizing, setCustomizing] = useState(false);
  const blocks = dashboardChosenBlocks(ctx);
  const showProjects = blocks.some(b => b.key === 'projects');
  const [deptFilter, setDeptFilter] = useState('All');
  const filtered = ctx.projects.filter(p => {
    if (filter !== 'All' && p.pipelineStatus !== filter) return false;
    // Header department scope (what this user is allowed to / chose to see)
    // is applied FIRST and is not overridable from here — the local
    // deptFilter chips below are a convenience narrowing on top of it.
    if (!projectInDepartment(p, ctx.activeDepartment, ctx.scopeLibrary)) return false;
    if (deptFilter !== 'All' && !p.companyDepartment.includes(deptFilter)) return false;
    if (search && !(p.name.toLowerCase().includes(search.toLowerCase()) || p.projectNumber.toLowerCase().includes(search.toLowerCase()))) return false;
    return true;
  });

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-5">
        <div>
          <h1 className="text-2xl font-bold">Dashboard</h1>
          <p className="text-sm text-[var(--leon-black)]/50">
            {ctx.currentUserName}{personTitle(ctx.currentUser) ? ` — ${personTitle(ctx.currentUser)}` : ''}
          </p>
        </div>
        <div className="flex items-center gap-2 flex-wrap">
          {showProjects && <TextInput placeholder="Search projects…" value={search} onChange={e => setSearch(e.target.value)} className="w-full sm:!w-56" />}
          <Button variant="outline" onClick={() => setCustomizing(true)}>⚙️ Customise</Button>
          <Button onClick={() => setShowNew(true)}>+ New Project</Button>
        </div>
      </div>

      {/* The chosen blocks. `projects` is the classic list and keeps its own
          filter bar below; everything else is a compact read. */}
      {blocks.filter(b => b.key !== 'projects').length > 0 && (
        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3 mb-6">
          {blocks.filter(b => b.key !== 'projects').map(b => (
            <DashboardBlock key={b.key} block={b}>
              <DashboardBlockBody blockKey={b.key} ctx={ctx} />
            </DashboardBlock>
          ))}
        </div>
      )}

      <DashboardCustomizeModal open={customizing} onClose={() => setCustomizing(false)} ctx={ctx} />

      {showProjects && (
        <p className="text-sm text-[var(--leon-black)]/50 mb-2">{filtered.length} project{filtered.length === 1 ? '' : 's'}</p>
      )}

      <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)] flex-wrap">
        {['All', ...PIPELINE_STATUSES].map(s => (
          <button
            key={s}
            onClick={() => setFilter(s)}
            className={`subtab-btn shrink-0 whitespace-nowrap px-3 py-1.5 text-[12px] font-semibold border-b-2 ${filter === s ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}
          >
            {s} {s !== 'All' && <span className="text-[var(--leon-black)]/30">({ctx.deptProjects(ctx.projects).filter(p => p.pipelineStatus === s).length})</span>}
          </button>
        ))}
      </div>

      <div className="flex gap-1 mb-5">
        {['All', ...COMPANY_DEPARTMENTS].map(d => (
          <button
            key={d}
            onClick={() => setDeptFilter(d)}
            className={`px-3 py-1 text-xs font-semibold rounded-full border ${deptFilter === d ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}
          >
            {d}
          </button>
        ))}
      </div>

      {filtered.length === 0 ? <EmptyState text="No projects match." /> : (
        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
          {filtered.map(p => <ProjectCard key={p.id} project={p} ctx={ctx} />)}
        </div>
      )}

      <NewProjectModal open={showNew} onClose={() => setShowNew(false)} ctx={ctx} />
    </div>
  );
}

function ProjectCard({ project, ctx }) {
  const account = ctx.accounts.find(a => a.id === project.accountId);
  const activeScope = project.scopes.find(s => !scopeIsComplete(s));
  const currentStage = activeScope ? activeScope.stages.find(s => s.status === 'In Progress' || s.status === 'Delayed') : null;
  return (
    // The picture runs the FULL HEIGHT of the card with everything else to its
    // right — a job is recognised by its photo before its name, and a 44px
    // square beside a heading was too small to recognise anything by. The band
    // is `relative` and the image inside is absolutely positioned, so the photo
    // never contributes to the card's height: the text decides how tall the
    // card is and the picture fills whatever that turns out to be. Grid items
    // stretch, so every card in a row matches.
    <div onClick={() => ctx.goProject(project.id)} className="hub-card cursor-pointer bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden flex items-stretch">
      <div className="relative w-24 sm:w-28 shrink-0 self-stretch bg-[var(--leon-cream)]">
        <ProjectThumb project={project} fill />
      </div>
      <div className="min-w-0 flex-1 p-3.5">
        <div className="flex items-center gap-2">
          <HealthDot level={project.health} />
          <span className="text-[11px] text-[var(--leon-black)]/40 font-medium">{project.projectNumber}</span>
        </div>
        {/* Two lines rather than a truncation — the text column is narrower now
            that the picture has a band of its own, and a job whose name is cut
            off is a job you have to open to identify. */}
        <h3 className="font-bold text-sm leading-tight line-clamp-2">{project.name}</h3>
        <p className="text-xs text-[var(--leon-black)]/50 truncate">{account ? account.name : '—'}</p>
        <div className="flex items-center gap-1.5 flex-wrap mt-2">
          <StatusBadge status={project.pipelineStatus} />
          <ComplexityStars level={project.complexity} />
          <Badge tone="neutral">{project.scopes.length} scope{project.scopes.length === 1 ? '' : 's'}</Badge>
        </div>
        {currentStage && (
          <p className="text-xs text-[var(--leon-black)]/50 border-t border-[var(--leon-line)] pt-2 mt-2">
            Current: <strong>{currentStage.name}</strong> · due {fmtDate(currentStage.plannedDue)}
            {currentStage.delayDays > 0 && <span className="text-[var(--leon-red)] font-semibold"> · +{currentStage.delayDays}d delay</span>}
          </p>
        )}
      </div>
    </div>
  );
}

const NEW_PROJECT_DEFAULTS = {
  projectNumber: '', name: '', accountId: '', newAccountName: '', department: 'Interior Finishes',
  companyDepartment: ['Interiors'], complexity: 'Medium', pipelineStatus: 'Lead',
  address: '', projectType: 'Residential', sizeSqFt: '', unitQuantity: '', buildingStories: '', laborType: 'Standard',
};
// Next sequential "LI-{year}-{seq}" project number, based on the highest
// existing sequence number issued for the current year.
function nextProjectNumber(projects) {
  const year = new Date().getFullYear();
  const prefix = `LI-${year}-`;
  const maxSeq = projects.reduce((max, p) => {
    if (!p.projectNumber || !p.projectNumber.startsWith(prefix)) return max;
    const seq = parseInt(p.projectNumber.slice(prefix.length), 10);
    return Number.isFinite(seq) && seq > max ? seq : max;
  }, 0);
  return `${prefix}${String(maxSeq + 1).padStart(3, '0')}`;
}
function NewProjectModal({ open, onClose, ctx }) {
  const [form, setForm] = useState(NEW_PROJECT_DEFAULTS);
  useEffect(() => { if (open) setForm({ ...NEW_PROJECT_DEFAULTS, accountId: ctx.accounts[0]?.id || '', projectNumber: nextProjectNumber(ctx.projects) }); }, [open]);
  function submit() {
    let accountId = form.accountId;
    if (accountId === '__new__') {
      if (!form.newAccountName.trim()) return;
      const acct = ctx.addAccount({ name: form.newAccountName, accountType: 'Other', contactName: '', title: '', email: '', phone: '', billingAddress: '' });
      accountId = acct.id;
    }
    if (!form.projectNumber || !form.name || !accountId) return;
    const p = ctx.addProject({ ...form, accountId });
    onClose();
    ctx.goProject(p.id);
  }
  return (
    <Modal open={open} onClose={onClose} title="New Project" wide footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create Project</Button></>}>
      <div className="space-y-3">
        <Field label="Project Number" hint="Assigned automatically"><TextInput value={form.projectNumber} disabled className="!bg-[var(--leon-cream)] !text-[var(--leon-black)]/60" /></Field>
        <Field label="Project Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <Field label="Account">
          <Select value={form.accountId} onChange={e => setForm({ ...form, accountId: e.target.value })}>
            {ctx.accounts.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
            <option value="__new__">+ New account…</option>
          </Select>
        </Field>
        {form.accountId === '__new__' && <Field label="New Account Name"><TextInput value={form.newAccountName} onChange={e => setForm({ ...form, newAccountName: e.target.value })} /></Field>}
        <Field label="Project Address">
          <AddressAutocomplete value={form.address}
            onChange={v => setForm({ ...form, address: v })}
            onPick={r => setForm(f => Object.assign({}, f, { address: r.line || r.label },
              r.state ? { addressState: r.state } : {}))} />
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Department/Division"><TextInput value={form.department} onChange={e => setForm({ ...form, department: e.target.value })} /></Field>
          <Field label="Company Department" hint="Select one or both">
            <div className="flex gap-1.5">
              {COMPANY_DEPARTMENTS.map(d => (
                <button key={d} type="button" onClick={() => setForm({ ...form, companyDepartment: form.companyDepartment.includes(d) ? form.companyDepartment.filter(x => x !== d) : [...form.companyDepartment, d] })}
                  className={`px-3 py-1.5 rounded-lg text-xs font-semibold border ${form.companyDepartment.includes(d) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60'}`}>
                  {d}
                </button>
              ))}
            </div>
          </Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Project Type">
            <Select value={form.projectType} onChange={e => setForm({ ...form, projectType: e.target.value })}>
              {PROJECT_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
            </Select>
          </Field>
          <Field label="Labor Type">
            <Select value={form.laborType} onChange={e => setForm({ ...form, laborType: e.target.value })}>
              {LABOR_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Size (Sq Ft)"><TextInput type="number" value={form.sizeSqFt} onChange={e => setForm({ ...form, sizeSqFt: e.target.value })} /></Field>
          <Field label="Unit Quantity"><TextInput type="number" value={form.unitQuantity} onChange={e => setForm({ ...form, unitQuantity: e.target.value })} /></Field>
          <Field label="Building Stories"><TextInput type="number" value={form.buildingStories} onChange={e => setForm({ ...form, buildingStories: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Complexity">
            <Select value={form.complexity} onChange={e => setForm({ ...form, complexity: e.target.value })}>
              {complexityOptions(form.complexity).map(c => <option key={c.id} value={c.name}>{c.name} ({c.multiplier}x)</option>)}
            </Select>
          </Field>
          <Field label="Pipeline Status">
            <Select value={form.pipelineStatus} onChange={e => setForm({ ...form, pipelineStatus: e.target.value })}>
              {PIPELINE_STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
            </Select>
          </Field>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================================
// Accounts
// ============================================================================
const ACCOUNTS_LIST_SUBTABS = [
  { key: 'accounts', label: 'Accounts', icon: '🏢' },
  // Every job on the books, in the hub that owns the client relationships —
  // the Projects nav lands on the dashboard, which is a working view, not a list.
  { key: 'projects', label: 'Projects', icon: '🏗️' },
  { key: 'contacts', label: 'All Contacts', icon: '📇' },
  // The map plots the jobs these accounts own, so it lives with them rather
  // than as a top-level destination of its own.
  { key: 'map', label: 'Map', icon: '🗺️' },
];
const ACCOUNT_SORT_OPTIONS = [
  { key: 'name-asc', label: 'Name (A–Z)' },
  { key: 'name-desc', label: 'Name (Z–A)' },
  { key: 'date-desc', label: 'Newest First' },
  { key: 'date-asc', label: 'Oldest First' },
];
const ACCOUNT_SORT_COMPARATORS = {
  'name-asc': (a, b) => textAsc(a.name, b.name),
  'name-desc': (a, b) => textDesc(a.name, b.name),
  'date-desc': (a, b) => dateDesc(a.createdDate, b.createdDate),
  'date-asc': (a, b) => dateAsc(a.createdDate, b.createdDate),
};
// Every job on the books, as a list. The Projects nav entry lands on the
// dashboard, which is a working view — cards, filters, what needs attention.
// This is the other question: "what have we got, and for whom", which belongs
// with the accounts that own them.
const ALL_PROJECTS_SORTS = [
  { key: 'name-asc', label: 'Name (A–Z)' },
  { key: 'number-desc', label: 'Project number (newest)' },
  { key: 'account-asc', label: 'Client (A–Z)' },
  { key: 'value-desc', label: 'Contract value (highest)' },
  { key: 'status-asc', label: 'Pipeline status' },
];

// The branded email, previewed as the recipient will actually see it. A
// sandboxed iframe renders the real HTML rather than a description of it, so
// the preview can never drift from what goes out — the same guarantee the Share
// modal makes.
function BrandedEmailPreview({ html, text }) {
  const [mode, setMode] = useState('html');
  return (
    <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden">
      <div className="flex items-center gap-2 px-3 py-1.5 bg-[var(--leon-cream)] border-b border-[var(--leon-line)]">
        <span className="text-[10px] font-semibold uppercase tracking-wide text-[var(--leon-black)]/45">Preview</span>
        <div className="ml-auto flex rounded border border-[var(--leon-line)] overflow-hidden">
          {[['html', 'Branded'], ['text', 'Plain text']].map(([k, l]) => (
            <button key={k} type="button" onClick={() => setMode(k)}
              className={`px-2 py-0.5 text-[11px] font-semibold ${mode === k ? 'bg-[var(--leon-brown)] text-white' : 'bg-white'}`}>{l}</button>
          ))}
        </div>
      </div>
      {mode === 'html'
        ? <iframe title="Email preview" sandbox="allow-same-origin" srcDoc={emailPreviewDocument(html)}
            className="w-full h-[420px] bg-white" />
        : <pre className="p-3 m-0 whitespace-pre-wrap text-[11px] font-sans text-[var(--leon-black)]/75 bg-white h-[420px] overflow-auto">{text}</pre>}
      <div className="px-3 py-2 bg-[var(--leon-cream)] border-t border-[var(--leon-line)] text-[11px] text-[var(--leon-black)]/55">
        The <b>branded</b> version is what is filed in the outbox and what a mail service will send once
        one exists. Your mail app receives the <b>plain text</b> version, because a pre-filled draft
        cannot carry HTML.
      </div>
    </div>
  );
}

// Your real signature is the designed banner you already use in Outlook or
// Gmail — a picture, not typed text. Upload it once and every branded email
// from the Hub carries it.
//
// The typed version is kept as the plain-text fallback and is not optional
// dressing: a `mailto:` draft cannot carry an image, so when the Hub hands a
// message to your mail client it is the text that travels. (Your mail client
// will usually append its own signature to that draft anyway, which is the one
// that actually reaches the client.)
// A signature exported from a design tool is routinely 4000px wide and several
// megabytes. Scaled down to email width it is a fraction of that, and nothing is
// lost: it is only ever displayed around 550px. Kept as PNG when it has
// transparency, otherwise re-encoded as JPEG, which is far smaller for a photo.
function shrinkSignature(dataUrl, maxWidth) {
  return new Promise(resolve => {
    const im = new Image();
    im.onload = () => {
      const scale = Math.min(1, maxWidth / im.width);
      const w = Math.round(im.width * scale), h = Math.round(im.height * scale);
      const c = document.createElement('canvas');
      c.width = w; c.height = h;
      const g = c.getContext('2d');
      g.drawImage(im, 0, 0, w, h);
      const png = c.toDataURL('image/png');
      const jpg = c.toDataURL('image/jpeg', 0.9);
      const url = jpg.length < png.length * 0.7 ? jpg : png;
      resolve({ url, w, h, bytes: Math.round(url.length * 0.75) });
    };
    im.onerror = () => resolve({ url: dataUrl, w: 0, h: 0, bytes: dataUrl.length });
    im.src = dataUrl;
  });
}

// Face ID / Touch ID on THIS device, and a plain account of what it does.
//
// It is a device lock, not authentication: with no server there is nobody to
// verify the signature against, so what this buys is that a laptop left open
// does not hand the Hub to whoever picks it up. It does not encrypt anything
// and it does not stop someone who knows their way around a browser. The panel
// says so, because a security control people misunderstand is worse than none.
function BiometricUnlockField({ person }) {
  const [available, setAvailable] = useState(null);
  const [enrolled, setEnrolled] = useState(() => bioEnrolledFor(person.id));
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState('');
  useEffect(() => { bioPlatformAvailable().then(setAvailable); }, []);

  function enroll() {
    setMsg(''); setBusy(true);
    bioEnroll(person)
      .then(() => { setEnrolled(true); setMsg('Set up. Next time you open the Hub on this device you can unlock with Face ID or Touch ID.'); })
      .catch(e => setMsg((e && e.name === 'NotAllowedError') ? 'Cancelled — nothing was changed.' : ((e && e.message) || 'Could not set that up.')))
      .then(() => setBusy(false), () => setBusy(false));
  }
  function forget() { bioForgetFor(person.id); setEnrolled(false); setMsg('Removed from this device.'); }

  return (
    <div className="space-y-2">
      <p className="text-[12px] text-[var(--leon-black)]/60">
        The Hub already keeps you signed in on a device you have used before — you are not asked for a
        password each time. Turning this on adds a confirmation step, so the session cannot simply be
        picked up by whoever opens the laptop next.
      </p>
      {available === false && (
        <p className="text-[12px] text-[var(--leon-black)]/50">
          This browser or device has no built-in biometric reader, so there is nothing to turn on here.
        </p>
      )}
      {available && (enrolled ? (
        <div className="flex items-center gap-2 flex-wrap">
          <Badge tone="green">On for this device</Badge>
          <Button size="sm" variant="outline" onClick={forget}>Turn off on this device</Button>
        </div>
      ) : (
        <Button size="sm" onClick={enroll} disabled={busy}>
          {busy ? 'Waiting for your device…' : 'Turn on Face ID / Touch ID here'}
        </Button>
      ))}
      {msg && <p className="text-[12px] text-[var(--leon-brown)]">{msg}</p>}
      <p className="text-[11px] text-[var(--leon-black)]/45">
        Set up per device, and only after signing in with your password — it is a shortcut in, never a
        way in. It locks the screen; it does not encrypt the data, and anyone who can open this
        browser&rsquo;s developer tools can still read what is stored. Real protection needs a server,
        which this app does not have.
      </p>
    </div>
  );
}

function EmailSignatureField({ form, setForm, person }) {
  const fileRef = useRef(null);
  const [err, setErr] = useState('');
  const [note, setNote] = useState('');
  const img = form.emailSignatureImage;

  async function onPick(e) {
    const f = e.target.files[0];
    e.target.value = '';
    if (!f) return;
    setErr('');
    if (!/^image\//.test(f.type)) { setErr('That is not an image. Export your signature as PNG or JPG.'); return; }
    // Signatures live on the person record, which persists to localStorage with
    // everything else — and a signature exported straight out of a design tool
    // is routinely 4000px wide. Rejecting it would just make people go and
    // resize it themselves, so it is resized HERE instead: nobody should have
    // to know what an email-safe signature weighs.
    const raw = await readFileAsDataURL(f);
    const { url, w, h, bytes } = await shrinkSignature(raw, 1100);
    if (bytes > 700 * 1024) {
      setErr('Even resized, that file is too heavy for an email signature. Try a simpler export.');
      return;
    }
    setNote(f.size > bytes * 1.2
      ? `Resized to ${w}×${h} (${Math.round(bytes / 1024)} KB) so it stays light in an email — your original file is untouched.`
      : '');
    setForm({ ...form, emailSignatureImage: url, emailSignatureImageName: f.name });
  }

  return (
    <div className="space-y-3">
      <p className="text-xs text-[var(--leon-black)]/55">
        Upload the signature file you already use in your email — the banner with your photo,
        title and contact details. It goes at the bottom of every branded email the Hub composes.
      </p>

      {img ? (
        <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden bg-[var(--leon-cream)]">
          <Photo src={img} alt="Your email signature" title="Your email signature" className="w-full h-auto block" />
          <div className="flex items-center gap-2 px-2.5 py-1.5 bg-white border-t border-[var(--leon-line)]">
            <span className="text-[11px] text-[var(--leon-black)]/50 truncate">{form.emailSignatureImageName || 'signature'}</span>
            <button type="button" onClick={() => fileRef.current.click()}
              className="ml-auto text-[11px] font-semibold text-[var(--leon-brown)]">Replace</button>
            <button type="button" onClick={() => setForm({ ...form, emailSignatureImage: null, emailSignatureImageName: '' })}
              className="text-[11px] font-semibold text-red-600">Remove</button>
          </div>
        </div>
      ) : (
        <button type="button" onClick={() => fileRef.current.click()}
          className="w-full rounded-lg border border-dashed border-[var(--leon-line)] hover:border-[var(--leon-brown)] p-6 text-center transition">
          <div className="text-2xl mb-1">🖋️</div>
          <div className="text-sm font-semibold">Upload your signature file</div>
          <div className="text-[11px] text-[var(--leon-black)]/45">PNG or JPG, around 1000px wide</div>
        </button>
      )}
      <input ref={fileRef} type="file" accept="image/*" className="hidden" onChange={onPick} />
      {err && <div className="rounded bg-red-50 border border-red-200 text-red-700 text-xs p-2.5">{err}</div>}
      {note && <div className="rounded bg-[var(--leon-cream)] border border-[var(--leon-line)] text-[var(--leon-black)]/65 text-xs p-2.5">{note}</div>}

      <div>
        <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">
          Plain-text version
        </p>
        <p className="text-[11px] text-[var(--leon-black)]/50 mb-1.5">
          Used where a picture cannot go — a draft handed to your mail app, and any client that
          blocks images. Leave it blank and your name, title and the company details are used.
        </p>
        <TextArea rows={4} value={form.emailSignature || ''}
          onChange={e => setForm({ ...form, emailSignature: e.target.value })}
          placeholder={`${person.name}${person.title ? `, ${person.title}` : ''}\nLEON INTEGRA\n617-559-0122`} />
      </div>
    </div>
  );
}

// ══════════════════════════════════════════ Quotation chasing
// A weekly cadence against every open quotation, running until it converts or
// dies. The queue is recomputed on every render from the quote revisions and
// the follow-up history — nothing is stored, so it can never be stale or wrong.
//
// It cannot send on its own: a browser with no backend cannot, and an API key
// cannot live in a page. What it does instead is work out who is due, write
// this week's message, and hand it to your mail client ready to go. You press
// send, and the follow-up logs itself against the right revision.
function QuoteChaseView({ ctx }) {
  const [showAll, setShowAll] = useState(false);
  const [open, setOpen] = useState(null);
  const queue = useMemo(
    () => buildQuoteChaseQueue(ctx.deptProjects(ctx.projects || []), ctx.accounts, todayISO()),
    [ctx.projects, ctx.accounts, ctx.activeDepartment]);
  const due = queue.filter(c => c.due);
  const shown = showAll ? queue : due;

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h3 className="text-lg font-bold">Quotation Follow-Ups</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            Every open quotation gets chased weekly until it becomes a job or is lost. Each week the
            message is different and each one is written against the client's latest revision — the same
            note six times is how a follow-up turns into noise.
          </p>
        </div>
        <div className="flex items-center gap-2">
          <Badge tone={due.length ? 'brown' : 'green'}>{due.length} due now</Badge>
          <Button size="sm" variant="ghost" onClick={() => setShowAll(!showAll)}>
            {showAll ? 'Only what is due' : `Show all ${queue.length}`}
          </Button>
        </div>
      </div>

      {!queue.length && (
        <EmptyState text="No open quotations to chase. A quote starts its cadence once the job is a Lead or Active Quotation and has a revision on it." />
      )}
      {!!queue.length && !shown.length && (
        <div className="rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-900">
          <b>Nothing due.</b> All {queue.length} open quotation{queue.length === 1 ? ' has' : 's have'} been
          chased within the last week. The next one comes up in {Math.min(...queue.map(c => c.dueIn))} day
          {Math.min(...queue.map(c => c.dueIn)) === 1 ? '' : 's'}.
        </div>
      )}

      <div className="space-y-2">
        {shown.map(c => (
          <div key={c.id} className={`rounded-lg border bg-white p-3 ${c.due ? 'border-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
            <div className="flex items-start gap-3 flex-wrap">
              <div className="min-w-0 flex-1">
                <button onClick={() => ctx.goProject(c.projectId)}
                  className="font-bold hover:text-[var(--leon-brown)] text-left leading-tight">{c.projectName}</button>
                <div className="text-xs text-[var(--leon-black)]/55">
                  {c.accountName} · {c.revLabel}{c.amount ? ` — ${fmtMoney(c.amount)}` : ''}
                </div>
              </div>
              <div className="text-right text-xs">
                <div className={c.due ? 'font-bold text-[var(--leon-brown)]' : 'text-[var(--leon-black)]/50'}>
                  {c.daysSince} day{c.daysSince === 1 ? '' : 's'} since last contact
                </div>
                <div className="text-[var(--leon-black)]/45">
                  {c.sentCount} chase{c.sentCount === 1 ? '' : 's'} sent on this revision
                  {!c.due && ` · next in ${c.dueIn}d`}
                </div>
              </div>
            </div>
            <div className="flex items-center gap-2 mt-2 pt-2 border-t border-[var(--leon-line)] flex-wrap">
              <Badge tone={c.exhausted ? 'neutral' : 'brown'}>Week {Math.min(c.round, QUOTE_CHASE_SEQUENCE.length)}</Badge>
              <span className="text-xs text-[var(--leon-black)]/60">{c.step.tone}</span>
              {c.exhausted && (
                <span className="text-[11px] text-[var(--leon-black)]/45">
                  — the sequence is finished; this one closes the loop rather than asking again
                </span>
              )}
              <div className="ml-auto flex items-center gap-2">
                <Button size="sm" variant="ghost" onClick={() => ctx.setQuoteChasePaused(c.projectId, true)}>Pause</Button>
                <Button size="sm" onClick={() => setOpen(c)}>📧 Write this week's</Button>
              </div>
            </div>
          </div>
        ))}
      </div>

      {queue.some(c => false) || null}
      <PausedChaseList ctx={ctx} />
      <QuoteChaseModal ctx={ctx} chase={open} onClose={() => setOpen(null)} />
    </div>
  );
}

function PausedChaseList({ ctx }) {
  const paused = ctx.deptProjects(ctx.projects || []).filter(p => p.quoteChasePaused);
  if (!paused.length) return null;
  return (
    <Collapsible id="chase-paused" title="Paused" count={paused.length}>
      <div className="space-y-1.5">
        {paused.map(p => (
          <div key={p.id} className="flex items-center gap-3 text-sm border border-[var(--leon-line)] rounded-lg px-3 py-2">
            <button onClick={() => ctx.goProject(p.id)} className="font-semibold hover:text-[var(--leon-brown)]">{p.name}</button>
            <Button size="sm" variant="ghost" className="ml-auto"
              onClick={() => ctx.setQuoteChasePaused(p.id, false)}>Resume chasing</Button>
          </div>
        ))}
      </div>
    </Collapsible>
  );
}

function QuoteChaseModal({ ctx, chase, onClose }) {
  const [message, setMessage] = useState('');
  const [sent, setSent] = useState(false);
  useEffect(() => {
    if (!chase) return;
    setSent(false);
    setMessage(fillChaseTemplate(chase.step.body, chase));
  }, [chase && chase.id]);
  if (!chase) return null;

  const contact = quoteChaseContact(chase.project);
  const subject = fillChaseTemplate(chase.step.subject, chase);
  const mail = buildQuoteFollowUpEmail({
    subject,
    recipientName: contact && contact.name,
    senderName: ctx.currentUserName, senderTitle: personTitle(ctx.currentUser),
    senderSignature: ctx.currentUser && ctx.currentUser.emailSignature,
    senderSignatureImage: ctx.currentUser && ctx.currentUser.emailSignatureImage,
    company: ctx.companyProfile, projectName: chase.projectName,
    revLabel: chase.revLabel,
    revAmount: chase.amount ? fmtMoney(chase.amount) : '',
    revDate: chase.revision.date ? fmtDate(chase.revision.date) : '',
    message,
  });
  const text = mail.body;

  function send() {
    if (!contact) return;
    // The branded version is filed as the record; the plain one is what the
    // mail client can actually be handed.
    ctx.fileOutgoingEmail({
      to: contact.email, toName: contact.name, subject, body: text, html: mail.html,
      projectId: chase.projectId,
    });
    window.location.href = buildMailtoUrl({
      to: contact.email, cc: (ctx.currentUser && ctx.currentUser.email) || '', subject, body: text,
    });
    // Logged against the revision it chased, so next week's message is the next
    // one in the sequence rather than the same one again.
    ctx.addFollowUp(chase.projectId, {
      date: todayISO(), method: 'Email', note: `Week ${chase.round} chase — ${chase.step.tone}.`,
      nextFollowUp: null, assigneeId: null,
      quoteRevisionId: chase.revision.id,
      sentTo: `${contact.name} <${contact.email}>`, emailed: true,
    });
    setSent(true);
  }

  return (
    <Modal open={!!chase} onClose={onClose} wide
      title={`Week ${Math.min(chase.round, QUOTE_CHASE_SEQUENCE.length)} — ${chase.step.tone}`}
      footer={
        sent
          ? <>
              <Button variant="ghost" onClick={() => navigator.clipboard.writeText(text)}>Copy the text</Button>
              <Button onClick={onClose}>Done</Button>
            </>
          : <>
              <Button variant="ghost" onClick={onClose}>Cancel</Button>
              <Button onClick={send} disabled={!contact}>📧 Open the email</Button>
            </>
      }>
      {sent ? (
        <div className="space-y-2 text-sm">
          <div className="text-3xl">📧</div>
          <p><b>Your email app should have opened, addressed to {contact.name}.</b> Press send there — the Hub cannot send it for you.</p>
          <p className="text-[var(--leon-black)]/60">
            The chase is logged against {chase.revLabel}, so next week this job moves to
            week {Math.min(chase.round + 1, QUOTE_CHASE_SEQUENCE.length)} of the sequence.
          </p>
        </div>
      ) : (
        <div className="space-y-3">
          <div className="grid gap-3 md:grid-cols-3 text-sm">
            <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Project</div>{chase.projectName}</div>
            <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Quotation</div>{chase.revLabel}{chase.amount ? ` — ${fmtMoney(chase.amount)}` : ''}</div>
            <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Silent for</div>{chase.daysSince} days</div>
          </div>
          {contact ? (
            <div className="text-sm">
              <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mr-2">To</span>
              <b>{contact.name}</b> <span className="text-[var(--leon-black)]/50">({contact.role}) — {contact.email}</span>
            </div>
          ) : (
            <div className="rounded bg-red-50 border border-red-200 text-red-700 text-sm p-3">
              This job has no contact with an email address. Add one under <b>Project Information &rarr; Contacts</b> —
              the <b>Estimator</b> is who a quotation is normally chased with.
            </div>
          )}
          <Field label="This week's message" hint="Written for this round. Edit it however you like.">
            <TextArea rows={5} value={message} onChange={e => setMessage(e.target.value)} />
          </Field>
          <BrandedEmailPreview html={mail.html} text={text} />
        </div>
      )}
    </Modal>
  );
}

// ---------------------------------------------------------------------------
// Sales — the pipeline, by stage
// ---------------------------------------------------------------------------
// One place for the commercial view of the business. It is deliberately a
// PROJECTION of `project.pipelineStatus`, not a second set of records: a lead
// becomes a quotation becomes a job by changing that one field, so nothing here
// can drift from what the rest of the Hub shows. Money comes from
// `salesValueSummary` / `latestQuoteAmount` / `revisedContractValue` in lib.jsx,
// which the Reports already use — one definition of "what a quote is worth".
const SALES_PIPELINE_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'leads', label: 'Leads', icon: '🌱', status: 'Lead' },
  { key: 'quotations', label: 'Quotations', icon: '📝', status: 'Active Quotation' },
  { key: 'jobs', label: 'Active Jobs', icon: '🏗️', status: 'Active Job' },
  { key: 'closed', label: 'Won & Lost', icon: '🏁' },
  // The Gulf operation quotes as a bill of quantities rather than a scope
  // estimate. It lives here because it IS the quotation for those jobs, and it
  // carries its own project picker so it does not depend on the tab above it.
  { key: 'boq', label: 'Bill of Quantities', icon: '🧾' },
];

function SalesHubView({ ctx }) {
  const [sub, setSub] = useHubSection(ctx, 'sales', 'overview');
  const [region, setRegion] = useState('All');
  const [boqProjectId, setBoqProjectId] = useState('');
  // A project's location follows its CLIENT — the account's region — because
  // that is what decides how the job is quoted, in what currency, and against
  // which scope classification. A standalone job with no account falls back to
  // the default location rather than disappearing from every filter.
  const inRegion = p => {
    if (region === 'All') return true;
    const acct = (ctx.accounts || []).find(a => a.id === p.accountId);
    return accountRegion(acct) === region;
  };
  const projects = ctx.deptProjects(ctx.projects || []).filter(inRegion);
  const counts = {};
  PIPELINE_STATUSES.forEach(st => { counts[st] = projects.filter(p => p.pipelineStatus === st).length; });

  return (
    <div data-print-region="Sales">
      <div className="mb-5">
        <h1 className="text-2xl font-bold">💼 Sales</h1>
        <p className="text-sm text-[var(--leon-black)]/50 max-w-3xl">
          The pipeline from first enquiry to signed job. Every row here is a real project — moving one
          on is changing its status, not copying it into another list.
        </p>
      </div>

      <div className="flex flex-wrap items-end gap-3 mb-4">
        <Field label="Location">
          <Select className="!w-48" value={region} onChange={e => setRegion(e.target.value)}>
            <option value="All">All locations</option>
            {ACCOUNT_REGIONS.map(r => <option key={r} value={r}>{accountRegionLabel(r)}</option>)}
          </Select>
        </Field>
        <p className="text-[12px] text-[var(--leon-black)]/50 max-w-lg">
          Follows the client&rsquo;s location. It narrows every tab below and the totals with them, so
          what you are looking at and what it adds up to always agree.
        </p>
      </div>

      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {SALES_PIPELINE_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold whitespace-nowrap border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>{t.label}
            {t.status && counts[t.status] ? <span className="ml-1.5 opacity-50">{counts[t.status]}</span> : null}
          </button>
        ))}
      </div>

      <HubTools title="Sales" heading="Sales" />

      {sub === 'overview' && <SalesOverview ctx={ctx} projects={projects} onGo={setSub} />}
      {sub === 'leads' && <SalesStageList ctx={ctx} projects={projects} status="Lead"
        blurb="Enquiries that have not been quoted yet. A lead with no quote revision is not in the chase queue — issue one and the weekly follow-up starts by itself." />}
      {sub === 'quotations' && <SalesStageList ctx={ctx} projects={projects} status="Active Quotation" showQuote
        blurb="Out with the client and not yet decided. The chase sequence counts against the LATEST revision, so issuing a new one restarts it." />}
      {sub === 'jobs' && <SalesStageList ctx={ctx} projects={projects} status="Active Job" showContract
        blurb="Won and in progress. Value here is the revised contract — the original plus approved change orders — not the quote it started as." />}
      {sub === 'closed' && <SalesClosed ctx={ctx} projects={projects} />}
      {sub === 'boq' && (typeof BoqSection === 'function'
        ? <BoqSection ctx={ctx} projectId={boqProjectId} onProject={setBoqProjectId} />
        : <EmptyState text="The bill-of-quantities module (softwares/boq.jsx) is not loaded." />)}
    </div>
  );
}

function SalesOverview({ ctx, projects, onGo }) {
  const sum = salesValueSummary(projects);
  const byStatus = st => projects.filter(p => p.pipelineStatus === st);
  const valueOf = list => list.reduce((n, p) => n + (latestQuoteAmount(p) || 0), 0);
  const leads = byStatus('Lead'), quotes = byStatus('Active Quotation');
  const jobs = byStatus('Active Job'), done = byStatus('Completed Job'), lost = byStatus('Lost Job');
  const contractOf = list => list.reduce((n, p) => n + (revisedContractValue(p) || 0), 0);

  const stages = [
    { key: 'leads', label: 'Leads', icon: '🌱', n: leads.length, money: valueOf(leads),
      note: 'Quoted value where a quote exists', tone: 'neutral' },
    { key: 'quotations', label: 'Quotations out', icon: '📝', n: quotes.length, money: sum.activeQuotationValue,
      note: 'Latest revision of each', tone: 'amber' },
    { key: 'jobs', label: 'Active jobs', icon: '🏗️', n: jobs.length, money: contractOf(jobs),
      note: 'Revised contract value', tone: 'green' },
    { key: 'closed', label: 'Completed', icon: '🏁', n: done.length, money: contractOf(done),
      note: 'Revised contract value', tone: 'green' },
    { key: 'closed', label: 'Lost', icon: '✕', n: lost.length, money: sum.lostQuotationValue,
      note: 'Value of the quote that was lost', tone: 'red' },
  ];

  return (
    <div className="space-y-5">
      <div className="grid sm:grid-cols-2 lg:grid-cols-5 gap-3">
        {stages.map(st => (
          <button key={st.label} onClick={() => onGo(st.key)}
            className="hub-card text-left bg-white rounded-xl border border-[var(--leon-line)] p-4 hover:-translate-y-0.5 hover:shadow-md transition">
            <div className="flex items-center gap-2 mb-1">
              <span aria-hidden="true">{st.icon}</span>
              <span className="text-[11px] font-bold uppercase tracking-wider text-[var(--leon-black)]/55">{st.label}</span>
            </div>
            <div className="text-2xl font-bold tabular-nums">{st.n}</div>
            <div className="text-sm font-semibold tabular-nums">{fmtMoney(st.money)}</div>
            <div className="text-[11px] text-[var(--leon-black)]/45 mt-1">{st.note}</div>
          </button>
        ))}
      </div>

      <Collapsible id="sales-conversion" title="Conversion" defaultOpen>
        <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
          <StatBox label="Win rate" value={sum.winRate === null ? '—' : sum.winRate.toFixed(0) + '%'} />
          <StatBox label="Won / lost" value={`${sum.awardedCount} / ${sum.lostCount}`} />
          <StatBox label="Quotations issued" value={sum.numQuotations} />
          <StatBox label="Total quoted" value={fmtMoney(sum.totalQuoted)} />
        </div>
        <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
          Win rate counts only DECIDED jobs — won over won-plus-lost. A quotation still out with the
          client is not counted either way, because it has not been decided yet.
        </p>
      </Collapsible>

      <Collapsible id="sales-by-person" title="By salesperson" defaultOpen>
        <SalesBySalesperson ctx={ctx} projects={projects} />
      </Collapsible>

      <Collapsible id="sales-by-account" title="By client">
        <SalesByAccount ctx={ctx} projects={projects} />
      </Collapsible>
    </div>
  );
}

// Who a project's salesperson is depends on the department, so this reads every
// department's team rather than assuming one.
function salesPeopleFor(ctx, project) {
  const out = [];
  (projectDepartments(project) || []).forEach(dep => {
    const id = teamMemberFor(project, 'Sales Person', dep);
    if (id && out.indexOf(id) === -1) out.push(id);
  });
  return out;
}

function SalesBySalesperson({ ctx, projects }) {
  const rows = {};
  projects.forEach(p => {
    const ids = salesPeopleFor(ctx, p);
    (ids.length ? ids : ['__none']).forEach(id => {
      const r = rows[id] || (rows[id] = { id, leads: 0, quotes: 0, jobs: 0, quoted: 0, contract: 0 });
      if (p.pipelineStatus === 'Lead') r.leads++;
      if (p.pipelineStatus === 'Active Quotation') { r.quotes++; r.quoted += latestQuoteAmount(p) || 0; }
      if (p.pipelineStatus === 'Active Job') { r.jobs++; r.contract += revisedContractValue(p) || 0; }
    });
  });
  const list = Object.values(rows).sort((a, b) => b.contract - a.contract);
  if (!list.length) return <EmptyState text="No projects in this department yet." />;
  return (
    <table className="w-full text-sm">
      <thead><tr className="text-left text-[11px] uppercase tracking-wider text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
        <th className="py-1.5">Salesperson</th><th className="text-right w-20">Leads</th>
        <th className="text-right w-24">Quotations</th><th className="text-right w-32">Quoted</th>
        <th className="text-right w-24">Active jobs</th><th className="text-right w-32">Contract</th></tr></thead>
      <tbody>
        {list.map(r => (
          <tr key={r.id} className="border-b border-[var(--leon-line)]/50">
            <td className="py-1.5 font-semibold">{r.id === '__none' ? <span className="text-[var(--leon-black)]/45">Not assigned</span> : personName(ctx, r.id)}</td>
            <td className="text-right tabular-nums">{r.leads || '—'}</td>
            <td className="text-right tabular-nums">{r.quotes || '—'}</td>
            <td className="text-right tabular-nums">{r.quoted ? fmtMoney(r.quoted) : '—'}</td>
            <td className="text-right tabular-nums">{r.jobs || '—'}</td>
            <td className="text-right tabular-nums">{r.contract ? fmtMoney(r.contract) : '—'}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function SalesByAccount({ ctx, projects }) {
  const rows = {};
  projects.forEach(p => {
    const key = p.accountId || '__none';
    const r = rows[key] || (rows[key] = { key, n: 0, quoted: 0, contract: 0 });
    r.n++; r.quoted += latestQuoteAmount(p) || 0; r.contract += revisedContractValue(p) || 0;
  });
  const list = Object.values(rows).sort((a, b) => b.contract - a.contract);
  if (!list.length) return <EmptyState text="No projects in this department yet." />;
  return (
    <table className="w-full text-sm">
      <thead><tr className="text-left text-[11px] uppercase tracking-wider text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
        <th className="py-1.5">Client</th><th className="text-right w-24">Projects</th>
        <th className="text-right w-32">Quoted</th><th className="text-right w-32">Contract</th></tr></thead>
      <tbody>
        {list.map(r => {
          const acct = (ctx.accounts || []).find(a => a.id === r.key);
          return (
            <tr key={r.key} className="border-b border-[var(--leon-line)]/50">
              <td className="py-1.5 font-semibold">
                {acct ? <button className="text-[var(--leon-brown)] hover:underline" onClick={() => ctx.goAccount(acct.id)}>{acct.name}</button>
                      : <span className="text-[var(--leon-black)]/45">Standalone</span>}
              </td>
              <td className="text-right tabular-nums">{r.n}</td>
              <td className="text-right tabular-nums">{r.quoted ? fmtMoney(r.quoted) : '—'}</td>
              <td className="text-right tabular-nums">{r.contract ? fmtMoney(r.contract) : '—'}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

// One list, four stages. Search, sort and a click through to the job itself —
// a stage list that cannot be opened is a report, not a working screen.
function SalesStageList({ ctx, projects, status, blurb, showQuote, showContract }) {
  const [q, setQ] = useState('');
  const [sort, setSort] = useState('value');
  const rows = projects.filter(p => p.pipelineStatus === status);
  const acctName = id => ((ctx.accounts || []).find(a => a.id === id) || {}).name || '';

  const filtered = rows.filter(p => {
    if (!q.trim()) return true;
    const hay = [p.name, p.projectNumber, acctName(p.accountId), p.address].join(' ').toLowerCase();
    return hay.indexOf(q.trim().toLowerCase()) >= 0;
  });
  const sorted = [...filtered].sort((a, b) => {
    if (sort === 'name') return (a.name || '').localeCompare(b.name || '');
    if (sort === 'client') return acctName(a.accountId).localeCompare(acctName(b.accountId));
    if (sort === 'date') return String(b.createdDate || '').localeCompare(String(a.createdDate || ''));
    const av = showContract ? revisedContractValue(a) : latestQuoteAmount(a);
    const bv = showContract ? revisedContractValue(b) : latestQuoteAmount(b);
    return (bv || 0) - (av || 0);
  });
  const total = sorted.reduce((n, p) => n + ((showContract ? revisedContractValue(p) : latestQuoteAmount(p)) || 0), 0);

  return (
    <div className="space-y-3">
      {blurb && <p className="text-sm text-[var(--leon-black)]/55 max-w-3xl">{blurb}</p>}
      <div className="flex flex-wrap items-end gap-2">
        <Field label="Search"><TextInput className="!w-64" placeholder="Project, number, client, address…"
          value={q} onChange={e => setQ(e.target.value)} /></Field>
        <Field label="Sort by">
          <Select className="!w-44" value={sort} onChange={e => setSort(e.target.value)}>
            <option value="value">{showContract ? 'Contract value' : 'Quoted value'}</option>
            <option value="name">Project name</option>
            <option value="client">Client</option>
            <option value="date">Newest first</option>
          </Select>
        </Field>
        <div className="ml-auto text-right">
          <div className="text-[11px] uppercase tracking-wider text-[var(--leon-black)]/50">
            {sorted.length === rows.length ? 'Total' : 'Total (filtered)'}
          </div>
          <div className="text-lg font-bold tabular-nums">{fmtMoney(total)}</div>
        </div>
      </div>

      {!sorted.length ? <EmptyState text={`Nothing at "${status}" right now.`} /> : (
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead><tr className="text-left text-[11px] uppercase tracking-wider text-[var(--leon-black)]/50 border-b border-[var(--leon-line)]">
              <th className="py-1.5">Project</th><th>Client</th><th>Department</th><th>Sales</th>
              {showQuote && <th>Latest quote</th>}
              <th className="text-right">{showContract ? 'Contract' : 'Quoted'}</th><th></th>
            </tr></thead>
            <tbody>
              {sorted.map(p => {
                const revs = [...(p.quoteRevisions || [])].sort((a, b) => b.revision - a.revision);
                const latest = revs[0];
                const sales = salesPeopleFor(ctx, p);
                return (
                  <tr key={p.id} className="border-b border-[var(--leon-line)]/50 hover:bg-[var(--leon-cream)]">
                    <td className="py-2">
                      <button className="font-semibold text-[var(--leon-brown)] hover:underline text-left"
                        onClick={() => ctx.goProject(p.id)}>{p.name}</button>
                      {p.projectNumber && <div className="text-[11px] text-[var(--leon-black)]/45">{p.projectNumber}</div>}
                    </td>
                    <td>{acctName(p.accountId) || <span className="text-[var(--leon-black)]/40">Standalone</span>}</td>
                    <td className="text-[12px]">{(projectDepartments(p) || []).join(', ') || '—'}</td>
                    <td className="text-[12px]">{sales.length ? sales.map(id => personName(ctx, id)).join(', ') : <span className="text-[var(--leon-black)]/40">Not assigned</span>}</td>
                    {showQuote && <td className="text-[12px]">{latest ? `R${latest.revision} · ${fmtDate(latest.date)}` : <span className="text-[var(--leon-black)]/40">Not quoted</span>}</td>}
                    <td className="text-right tabular-nums font-semibold">
                      {fmtMoney(showContract ? revisedContractValue(p) : latestQuoteAmount(p))}
                    </td>
                    <td className="text-right"><Button size="sm" variant="outline" onClick={() => ctx.goProject(p.id)}>Open</Button></td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function SalesClosed({ ctx, projects }) {
  return (
    <div className="space-y-5">
      <Collapsible id="sales-won" title="Won — completed jobs" defaultOpen>
        <SalesStageList ctx={ctx} projects={projects} status="Completed Job" showContract />
      </Collapsible>
      <Collapsible id="sales-lost" title="Lost" defaultOpen>
        <SalesStageList ctx={ctx} projects={projects} status="Lost Job" showQuote
          blurb="Kept, not deleted — a lost job is the other half of the win rate, and the reason is worth reading before the next quote goes out." />
      </Collapsible>
    </div>
  );
}

function AllProjectsListView({ ctx }) {
  const [q, setQ] = useState('');
  const [status, setStatus] = useState('All');
  const [acct, setAcct] = useState('All');
  const [sort, setSort] = useState('name-asc');

  const rows = useMemo(() => {
    const acctName = id => {
      const a = ctx.accounts.find(x => x.id === id);
      return a ? a.name : '';
    };
    // deptProjects is a FILTER, not a list — it takes the projects and returns
    // the ones in the active department.
    let out = ctx.deptProjects(ctx.projects || []).map(p => ({
      p, account: acctName(p.accountId),
      value: Number(p.originalContractValue) || 0,
      scopes: (p.scopes || []).length,
      closed: p.closeout && p.closeout.status === 'Closed',
    }));
    if (status !== 'All') out = out.filter(r => r.p.pipelineStatus === status);
    if (acct !== 'All') out = out.filter(r => r.p.accountId === acct);
    const needle = q.trim().toLowerCase();
    if (needle) {
      out = out.filter(r => [r.p.name, r.p.projectNumber, r.account, r.p.address]
        .some(v => (v || '').toLowerCase().includes(needle)));
    }
    const cmp = {
      'name-asc': (a, b) => a.p.name.localeCompare(b.p.name),
      'number-desc': (a, b) => (b.p.projectNumber || '').localeCompare(a.p.projectNumber || ''),
      'account-asc': (a, b) => a.account.localeCompare(b.account) || a.p.name.localeCompare(b.p.name),
      'value-desc': (a, b) => b.value - a.value,
      'status-asc': (a, b) => (a.p.pipelineStatus || '').localeCompare(b.p.pipelineStatus || ''),
    }[sort];
    return [...out].sort(cmp);
  }, [ctx.projects, ctx.accounts, ctx.deptProjects, q, status, acct, sort]);

  const totalValue = rows.reduce((a, r) => a + r.value, 0);
  const statuses = [...new Set((ctx.projects || []).map(p => p.pipelineStatus).filter(Boolean))];

  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-center gap-2">
        <TextInput className="!w-64" value={q} onChange={e => setQ(e.target.value)}
          placeholder="Search by project, number, client or address…" />
        <Select className="!w-auto" value={status} onChange={e => setStatus(e.target.value)}>
          <option value="All">All statuses</option>
          {statuses.map(x => <option key={x}>{x}</option>)}
        </Select>
        <Select className="!w-auto" value={acct} onChange={e => setAcct(e.target.value)}>
          <option value="All">All clients</option>
          {[...ctx.accounts].sort((a, b) => a.name.localeCompare(b.name))
            .map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
        </Select>
        <Select className="!w-auto" value={sort} onChange={e => setSort(e.target.value)}>
          {ALL_PROJECTS_SORTS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
        </Select>
        <span className="ml-auto text-xs text-[var(--leon-black)]/50">
          {rows.length} project{rows.length === 1 ? '' : 's'}
          {ctx.canSeeFin && totalValue ? ` · ${fmtMoney(totalValue)} contracted` : ''}
        </span>
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-sm min-w-[820px]">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-3 py-2">Project</th>
              <th className="px-3 py-2">Client</th>
              <th className="px-3 py-2">Status</th>
              <th className="px-3 py-2">Department</th>
              <th className="px-3 py-2 text-right">Scopes</th>
              {ctx.canSeeFin && <th className="px-3 py-2 text-right">Contract</th>}
              <th className="px-3 py-2">Address</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(({ p, account, value, scopes, closed }) => (
              <tr key={p.id} onClick={() => ctx.goProject(p.id)}
                className="border-b border-[var(--leon-line)]/60 cursor-pointer hover:bg-[var(--leon-cream)]">
                <td className="px-3 py-2">
                  <div className="flex items-center gap-2">
                    <HealthDot level={p.health} />
                    <div className="min-w-0">
                      <div className="font-semibold leading-tight truncate">{p.name}</div>
                      <div className="text-[11px] text-[var(--leon-black)]/45">{p.projectNumber}</div>
                    </div>
                    {closed && <Badge tone="green">Closed</Badge>}
                  </div>
                </td>
                <td className="px-3 py-2 text-[var(--leon-black)]/70">{account || '—'}</td>
                <td className="px-3 py-2"><StatusBadge status={p.pipelineStatus} /></td>
                <td className="px-3 py-2 text-[11px] text-[var(--leon-black)]/55">
                  {(projectDepartments(p) || []).join(' · ') || '—'}
                </td>
                <td className="px-3 py-2 text-right tabular-nums">{scopes}</td>
                {ctx.canSeeFin && <td className="px-3 py-2 text-right tabular-nums">{value ? fmtMoney(value) : '—'}</td>}
                <td className="px-3 py-2 text-[11px] text-[var(--leon-black)]/50 truncate max-w-[220px]">{p.address || '—'}</td>
              </tr>
            ))}
            {!rows.length && (
              <tr><td colSpan={ctx.canSeeFin ? 7 : 6} className="px-3 py-6 text-center text-[var(--leon-black)]/40">
                No projects match those filters.
              </td></tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function AccountsView({ ctx }) {
  const [showNew, setShowNew] = useState(false);
  const [viewTab, setViewTab] = useHubSection(ctx, 'accounts', 'accounts');
  const [search, setSearch] = useState('');
  const [typeFilter, setTypeFilter] = useState('All');
  const [regionFilter, setRegionFilter] = useState('All');
  const [sortKey, setSortKey] = useState('name-asc');
  const blankForm = { name: '', accountType: ACCOUNT_TYPES[0], region: ACCOUNT_REGIONS[0], contactName: '', title: '', email: '', phone: '', contactMobile: '', website: '', billingAddress: '', notes: '' };
  const [form, setForm] = useState(blankForm);

  function submit() {
    if (!form.name.trim()) return;
    ctx.addAccount(form);
    setForm(blankForm);
    setShowNew(false);
  }

  const q = search.trim().toLowerCase();
  const filtered = ctx.accounts.filter(a => {
    if (typeFilter !== 'All' && a.accountType !== typeFilter) return false;
    if (regionFilter !== 'All' && (a.region || ACCOUNT_REGIONS[0]) !== regionFilter) return false;
    if (!q) return true;
    return [a.name, a.contactName, a.email].some(v => (v || '').toLowerCase().includes(q));
  });
  const sorted = sortList(filtered, sortKey, ACCOUNT_SORT_COMPARATORS);

  return (
    <div>
      <div className="flex items-center justify-between mb-5">
        <h1 className="text-2xl font-bold">Accounts</h1>
        <Button onClick={() => setShowNew(true)}>+ New Account</Button>
      </div>

      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)]">
        {ACCOUNTS_LIST_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setViewTab(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${viewTab === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>

      <HubTools title="Accounts" heading="Accounts" />

      {viewTab === 'map' ? <MapView ctx={ctx} />
        : viewTab === 'projects' ? <AllProjectsListView ctx={ctx} />
        : viewTab === 'contacts' ? <AllAccountContactsView ctx={ctx} /> : (
        <>
          <div className="flex flex-wrap items-center gap-2 mb-4">
            <TextInput value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by name, contact, or email…" className="!w-64" />
            <Select value={typeFilter} onChange={e => setTypeFilter(e.target.value)} className="!w-auto">
              <option value="All">All Types</option>
              {ACCOUNT_TYPES.map(t => <option key={t}>{t}</option>)}
            </Select>
            <Select value={regionFilter} onChange={e => setRegionFilter(e.target.value)} className="!w-auto">
              <option value="All">All Locations</option>
              {ACCOUNT_REGIONS.map(r => <option key={r} value={r}>{accountRegionLabel(r)}</option>)}
            </Select>
            <SortSelect value={sortKey} onChange={setSortKey} options={ACCOUNT_SORT_OPTIONS} />
            <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{sorted.length} of {ctx.accounts.length} account{ctx.accounts.length === 1 ? '' : 's'}</span>
          </div>
          <div className="space-y-3">
            {sorted.map(a => {
              const projects = ctx.projects.filter(p => p.accountId === a.id);
              const rate = acceptanceRate(projects);
              return (
                <div key={a.id} onClick={() => ctx.goAccountDetail(a.id)} className="hub-card cursor-pointer bg-white border border-[var(--leon-line)] rounded-xl p-4">
                  <div className="flex flex-wrap items-start justify-between gap-3">
                    <div className="flex items-center gap-3 min-w-0">
                      <Avatar name={a.name} url={a.logoUrl} size={40} />
                      <div>
                        <div className="flex items-center gap-2 flex-wrap">
                          <h3 className="font-bold">{a.name}</h3>
                          <Badge tone="neutral">{a.accountType}</Badge>
                          <Badge tone="brown">{accountRegionLabel(accountRegion(a))}</Badge>
                        </div>
                        <p className="text-xs text-[var(--leon-black)]/50">{a.contactName}{a.title ? `, ${a.title}` : ''} {a.email && `· ${a.email}`} {a.phone && `· ${a.phone}`}</p>
                      </div>
                    </div>
                    <div className="flex items-center gap-2 shrink-0">
                      {rate.pct !== null && <Badge tone={rate.pct >= 50 ? 'green' : 'yellow'}>{fmtPct(rate.pct)} acceptance</Badge>}
                      <Badge tone="neutral">{projects.length} project{projects.length === 1 ? '' : 's'}</Badge>
                    </div>
                  </div>
                </div>
              );
            })}
            {sorted.length === 0 && <EmptyState text="No accounts match." />}
          </div>
        </>
      )}
      <Modal open={showNew} onClose={() => setShowNew(false)} title="New Account" footer={<><Button variant="ghost" onClick={() => setShowNew(false)}>Cancel</Button><Button onClick={submit}>Add Account</Button></>}>
        <div className="space-y-3">
          <div className="grid grid-cols-2 gap-3">
            <Field label="Company Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
            <Field label="Company Type"><Select value={form.accountType} onChange={e => setForm({ ...form, accountType: e.target.value })}>{ACCOUNT_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
          <Field label="Location"><Select value={form.region || ACCOUNT_REGIONS[0]} onChange={e => setForm({ ...form, region: e.target.value })}>{ACCOUNT_REGIONS.map(t => <option key={t} value={t}>{accountRegionLabel(t)}</option>)}</Select></Field>
            <Field label="Location"><Select value={form.region || ACCOUNT_REGIONS[0]} onChange={e => setForm({ ...form, region: e.target.value })}>{ACCOUNT_REGIONS.map(t => <option key={t} value={t}>{accountRegionLabel(t)}</option>)}</Select></Field>
          </div>
          <Field label="Contact Name"><TextInput value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></Field>
          <Field label="Title"><TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} /></Field>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
            <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
          </div>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Mobile"><TextInput value={form.contactMobile} onChange={e => setForm({ ...form, contactMobile: e.target.value })} /></Field>
            <Field label="Website"><TextInput value={form.website} onChange={e => setForm({ ...form, website: e.target.value })} placeholder="e.g. www.company.com" /></Field>
          </div>
          <Field label="Billing Address"><TextInput value={form.billingAddress} onChange={e => setForm({ ...form, billingAddress: e.target.value })} /></Field>
          <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        </div>
      </Modal>
    </div>
  );
}

const ALL_CONTACTS_SORT_OPTIONS = [
  { key: 'name-asc', label: 'Name (A–Z)' },
  { key: 'name-desc', label: 'Name (Z–A)' },
  { key: 'account-asc', label: 'Account (A–Z)' },
  { key: 'date-desc', label: 'Newest First' },
  { key: 'date-asc', label: 'Oldest First' },
];
const ALL_CONTACTS_SORT_COMPARATORS = {
  'name-asc': (a, b) => textAsc(a.name, b.name),
  'name-desc': (a, b) => textDesc(a.name, b.name),
  'account-asc': (a, b) => textAsc(a.accountName, b.accountName),
  'date-desc': (a, b) => dateDesc(a.createdDate, b.createdDate),
  'date-asc': (a, b) => dateAsc(a.createdDate, b.createdDate),
};
// Every contact logged under every account, in one searchable/sortable
// place — pulled live from account.contacts (no separate data store), so it
// can never drift out of sync with each account's own Contact Log.
function AllAccountContactsView({ ctx }) {
  const [search, setSearch] = useState('');
  const [roleFilter, setRoleFilter] = useState('All');
  const [sortKey, setSortKey] = useState('name-asc');
  const [viewingPhotoId, setViewingPhotoId] = useState(null);

  const allContacts = useMemo(() => (
    ctx.accounts.flatMap(a => a.contacts.map(c => ({ ...c, accountName: a.name, accountId: a.id })))
  ), [ctx.accounts]);

  const q = search.trim().toLowerCase();
  const filtered = allContacts.filter(c => {
    if (roleFilter !== 'All' && c.role !== roleFilter) return false;
    if (!q) return true;
    return [c.name, c.accountName, c.email, c.title].some(v => (v || '').toLowerCase().includes(q));
  });
  const sorted = sortList(filtered, sortKey, ALL_CONTACTS_SORT_COMPARATORS);
  const viewingContact = allContacts.find(c => `${c.accountId}:${c.id}` === viewingPhotoId);

  return (
    <div>
      <div className="flex flex-wrap items-center gap-2 mb-4">
        <TextInput value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by name, account, title, or email…" className="!w-64" />
        <Select value={roleFilter} onChange={e => setRoleFilter(e.target.value)} className="!w-auto">
          <option value="All">All Roles</option>
          {ACCOUNT_CONTACT_ROLES.map(r => <option key={r}>{r}</option>)}
        </Select>
        <SortSelect value={sortKey} onChange={setSortKey} options={ALL_CONTACTS_SORT_OPTIONS} />
        <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{sorted.length} of {allContacts.length} contact{allContacts.length === 1 ? '' : 's'}</span>
      </div>
      {sorted.length === 0 ? <EmptyState text="No contacts match." /> : (
        <div className="space-y-1.5">
          {sorted.map(c => (
            <div key={`${c.accountId}:${c.id}`} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs bg-white">
              <div className="flex items-center gap-2 min-w-0">
                <button type="button" onClick={() => c.photoUrl && setViewingPhotoId(`${c.accountId}:${c.id}`)} title={c.photoUrl ? 'View photo larger' : ''} className={c.photoUrl ? 'cursor-pointer shrink-0' : 'shrink-0'}>
                  <Avatar name={c.name} url={c.photoUrl} size={28} />
                </button>
                <div className="min-w-0">
                  <div><Badge tone="neutral">{c.role}</Badge> <span className="font-semibold ml-1">{c.name}</span>{c.title ? ` — ${c.title}` : ''}</div>
                  <button type="button" onClick={() => ctx.goAccountDetail(c.accountId)} className="text-[var(--leon-brown)] font-semibold hover:underline">{c.accountName}</button>
                </div>
              </div>
              <span className="text-[var(--leon-black)]/50 shrink-0">{c.phone}{c.mobile ? ` / ${c.mobile}` : ''} {c.email && `· ${c.email}`}</span>
            </div>
          ))}
        </div>
      )}
      {viewingContact && <AttachmentViewerModal name={`${viewingContact.name} — Photo`} url={viewingContact.photoUrl} onClose={() => setViewingPhotoId(null)} />}
    </div>
  );
}

const ACCOUNT_DETAIL_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'contacts', label: 'Contacts', icon: '📇' },
  { key: 'projects', label: 'Projects', icon: '🏗️' },
  { key: 'billing', label: 'Billing', icon: '🏦' },
];
// Account Contact Log — same UI pattern as VendorContactsBlock, pointed at
// account.contacts/addAccountContact/removeAccountContact instead.
function blankAccountContactForm() { return { role: ACCOUNT_CONTACT_ROLES[0], name: '', title: '', phone: '', mobile: '', email: '', preferredContactMethod: '', notes: '', photoUrl: null }; }
function AccountContactsBlock({ ctx, account }) {
  const [adding, setAdding] = useState(false);
  const [form, setForm] = useState(blankAccountContactForm());
  const [viewingPhotoId, setViewingPhotoId] = useState(null);
  const viewingContact = account.contacts.find(c => c.id === viewingPhotoId);
  function submit() {
    if (!form.name.trim()) return;
    ctx.addAccountContact(account.id, form);
    setForm(blankAccountContactForm());
    setAdding(false);
  }
  const isAdmin = ctx.currentRole === 'Admin';
  return (
    <Collapsible title="Contact Log" count={account.contacts.length} right={<Button size="sm" variant="ghost" onClick={() => setAdding(a => !a)}>+ Add Contact</Button>}>
      {adding && (
        <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-3 space-y-2">
          <div className="flex items-center gap-2">
            <ImagePicker url={form.photoUrl} onChange={url => setForm({ ...form, photoUrl: url })} size={44} shape="circle" />
            <span className="text-[11px] text-[var(--leon-black)]/40">Photo (optional)</span>
          </div>
          <div className="grid grid-cols-2 gap-2">
            <Select value={form.role} onChange={e => setForm({ ...form, role: e.target.value })}>{ACCOUNT_CONTACT_ROLES.map(r => <option key={r}>{r}</option>)}</Select>
            <TextInput placeholder="Name" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} />
            <TextInput placeholder="Email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
          </div>
          <div className="grid grid-cols-3 gap-2">
            <TextInput placeholder="Phone" value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} />
            <TextInput placeholder="Mobile" value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} />
            <Select value={form.preferredContactMethod} onChange={e => setForm({ ...form, preferredContactMethod: e.target.value })}>
              <option value="">Preferred Method — none set</option>
              {PREFERRED_CONTACT_METHODS.map(m => <option key={m}>{m}</option>)}
            </Select>
          </div>
          <TextArea placeholder="Notes" rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} />
          <div className="flex justify-end gap-2"><Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button><Button size="sm" onClick={submit}>Save Contact</Button></div>
        </div>
      )}
      {account.contacts.length === 0 ? <EmptyState text="No additional contacts logged for this account." /> : (
        <div className="space-y-1.5">
          {account.contacts.map(c => (
            <div key={c.id} className="border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs">
              <div className="flex items-center justify-between gap-2">
                <div className="flex items-center gap-2">
                  <button type="button" onClick={() => c.photoUrl && setViewingPhotoId(c.id)} title={c.photoUrl ? 'View photo larger' : ''} className={c.photoUrl ? 'cursor-pointer' : ''}>
                    <Avatar name={c.name} url={c.photoUrl} size={28} />
                  </button>
                  <div><Badge tone="neutral">{c.role}</Badge> <span className="font-semibold ml-1">{c.name}</span>{c.title ? ` — ${c.title}` : ''}</div>
                </div>
                <div className="flex items-center gap-2">
                  <span className="text-[var(--leon-black)]/50">{c.phone}{c.mobile ? ` / ${c.mobile}` : ''} {c.email && `· ${c.email}`}</span>
                  {isAdmin && <IconBtn title="Remove (Admin only)" onClick={() => ctx.removeAccountContact(account.id, c.id)}>✕</IconBtn>}
                </div>
              </div>
              {(c.preferredContactMethod || c.notes) && (
                <p className="text-[var(--leon-black)]/40 mt-1">{c.preferredContactMethod && `Prefers: ${c.preferredContactMethod}`}{c.preferredContactMethod && c.notes ? ' · ' : ''}{c.notes}</p>
              )}
            </div>
          ))}
        </div>
      )}
      {viewingContact && <AttachmentViewerModal name={`${viewingContact.name} — Photo`} url={viewingContact.photoUrl} onClose={() => setViewingPhotoId(null)} />}
    </Collapsible>
  );
}
function EditAccountModal({ open, onClose, ctx, account }) {
  const [form, setForm] = useState(account);
  useEffect(() => { if (open) setForm(account); }, [open, account]);
  function submit() {
    if (!form.name.trim()) return;
    ctx.updateAccount(account.id, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Edit Account" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Company Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
          <Field label="Company Type"><Select value={form.accountType} onChange={e => setForm({ ...form, accountType: e.target.value })}>{ACCOUNT_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Contact Name"><TextInput value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></Field>
          <Field label="Title"><TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
          <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Mobile"><TextInput value={form.contactMobile} onChange={e => setForm({ ...form, contactMobile: e.target.value })} /></Field>
          <Field label="Website"><TextInput value={form.website} onChange={e => setForm({ ...form, website: e.target.value })} placeholder="e.g. www.company.com" /></Field>
        </div>
        <Field label="Billing Address"><TextInput value={form.billingAddress} onChange={e => setForm({ ...form, billingAddress: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
// Admin-only: grant/manage a client contact's login to the read-only Client
// Portal (ClientPortal, above). Creation only here — once created it's a
// normal teamDirectory entry, so Set Password/Deactivate already work from
// Users (UsersView) without duplicating that UI.
function blankClientLoginForm() { return { name: '', email: '', username: '', password: '' }; }
function AccountClientPortalBlock({ ctx, account }) {
  const [adding, setAdding] = useState(false);
  const [form, setForm] = useState(blankClientLoginForm());
  const logins = ctx.teamDirectory.filter(p => p.accountId === account.id && p.securityRole === 'Client');
  function submit() {
    if (!form.username.trim()) return;
    ctx.addClientPortalLogin(account.id, form);
    setForm(blankClientLoginForm());
    setAdding(false);
  }
  return (
    <Collapsible title="Client Portal Access" count={logins.length} right={<Button size="sm" variant="ghost" onClick={() => setAdding(a => !a)}>+ Add Client Login</Button>}>
      <p className="text-xs text-[var(--leon-black)]/50 mb-2">Gives someone at this account a read-only login to see their own jobs' stage progress, shop drawing records, contract overview, receivables, and upcoming deliveries/installations — nothing else.</p>
      {adding && (
        <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-3 space-y-2">
          <div className="grid grid-cols-2 gap-2">
            <TextInput placeholder="Name" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
            <TextInput placeholder="Email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <TextInput placeholder="Username" value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} />
            <TextInput placeholder="Password" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} />
          </div>
          <div className="flex justify-end gap-2"><Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button><Button size="sm" onClick={submit}>Create Login</Button></div>
        </div>
      )}
      {logins.length === 0 ? <EmptyState text="No client portal logins yet." /> : (
        <div className="space-y-1.5">
          {logins.map(p => (
            <div key={p.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs">
              <div><span className="font-semibold">{p.name}</span> <span className="text-[var(--leon-black)]/50">— {p.username} · {p.email}</span></div>
              {p.active ? <Badge tone="green">Active</Badge> : <Badge tone="neutral">Inactive</Badge>}
            </div>
          ))}
        </div>
      )}
    </Collapsible>
  );
}
function AccountDetailView({ ctx, account }) {
  const [sub, setSub] = useState('overview');
  const [filter, setFilter] = useState('All');
  const [showLogo, setShowLogo] = useState(false);
  const [editing, setEditing] = useState(false);
  const isAdmin = ctx.currentRole === 'Admin';
  const allProjects = ctx.projects.filter(p => p.accountId === account.id);
  const filtered = allProjects.filter(p => filter === 'All' || p.pipelineStatus === filter);
  const rate = acceptanceRate(allProjects);

  function exportAccountCsv() {
    downloadCsv(`account-${account.name}`,
      [{ key: 'name', label: 'Account' }, { key: 'contactName', label: 'Contact' }, { key: 'email', label: 'Email' }, { key: 'phone', label: 'Phone' }, { key: 'contactMobile', label: 'Mobile' }, { key: 'website', label: 'Website' }, { key: 'billingAddress', label: 'Billing Address' }, { key: 'totalProjects', label: 'Total Projects' }, { key: 'activeJobs', label: 'Active Jobs' }],
      [{ ...account, totalProjects: allProjects.length, activeJobs: allProjects.filter(p => p.pipelineStatus === 'Active Job').length }]
    );
  }
  return (
    <div>
      <button onClick={ctx.goAccounts} className="no-print text-sm text-[var(--leon-brown)] font-semibold mb-3">← Back to Accounts</button>

      <div className="no-print flex justify-end gap-2 mb-2">
        <PrintButton onClick={() => window.print()} />
        <Button variant="outline" size="sm" onClick={exportAccountCsv}>Export CSV</Button>
      </div>
      <div className="print-only print-area p-8">
        <PrintDocHeader title={account.name} meta={account.contactName} />
        <table className="w-full text-sm mb-4">
          <tbody>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold w-1/3">Contact</td><td className="py-1">{account.contactName}{account.title ? `, ${account.title}` : ''}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Email / Phone</td><td className="py-1">{account.email} {account.phone}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Billing Address</td><td className="py-1">{account.billingAddress || '—'}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Total Projects</td><td className="py-1">{allProjects.length}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Active Jobs</td><td className="py-1">{allProjects.filter(p => p.pipelineStatus === 'Active Job').length}</td></tr>
          </tbody>
        </table>
      </div>

      <div className="no-print">
        <div className="bg-white border border-[var(--leon-line)] rounded-xl p-4 mb-4">
          <div className="flex items-start justify-between gap-3 flex-wrap">
            <div className="flex items-center gap-3">
              <div>
                <ImagePicker url={account.logoUrl} onChange={url => ctx.updateAccount(account.id, { logoUrl: url })} size={56} />
                {account.logoUrl && <button type="button" onClick={() => setShowLogo(true)} className="block text-[11px] text-[var(--leon-brown)] font-semibold mt-1 hover:underline">View Larger</button>}
              </div>
              <div>
                <h1 className="text-xl font-bold">{account.name}</h1>
                <p className="text-sm text-[var(--leon-black)]/50">{account.contactName}{account.title ? `, ${account.title}` : ''} {account.email && `· ${account.email}`} {account.phone && `· ${account.phone}`}{account.contactMobile && ` · ${account.contactMobile}`}</p>
                {account.website && <p className="text-xs text-[var(--leon-black)]/40 mt-0.5">{account.website}</p>}
                {account.billingAddress && <p className="text-xs text-[var(--leon-black)]/40 mt-1">{account.billingAddress}</p>}
                {account.notes && <p className="text-xs text-[var(--leon-black)]/40 mt-1 italic">{account.notes}</p>}
              </div>
            </div>
            <div className="flex items-center gap-2 shrink-0">
              {isAdmin && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>Edit Account</Button>}
              <Select value={account.accountType} onChange={e => ctx.updateAccount(account.id, { accountType: e.target.value })} className="!w-52 !py-1 !text-xs">
                {ACCOUNT_TYPES.map(t => <option key={t}>{t}</option>)}
              </Select>
              <Select value={account.region || ACCOUNT_REGIONS[0]} onChange={e => ctx.updateAccount(account.id, { region: e.target.value })} className="!w-40 !py-1 !text-xs">
                {ACCOUNT_REGIONS.map(t => <option key={t} value={t}>{accountRegionLabel(t)}</option>)}
              </Select>
            </div>
          </div>
        </div>
        {showLogo && <AttachmentViewerModal name={`${account.name} — Logo`} url={account.logoUrl} onClose={() => setShowLogo(false)} />}
        {isAdmin && <EditAccountModal open={editing} onClose={() => setEditing(false)} ctx={ctx} account={account} />}

        <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
          {ACCOUNT_DETAIL_SUBTABS.map(t => (
            <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
          ))}
        </div>
        <HubTools />

        {sub === 'overview' && (
          <>
            <div className="grid sm:grid-cols-3 gap-3 mb-5">
              <StatBox label="Total Projects" value={String(allProjects.length)} />
              <StatBox label="Active Jobs" value={String(allProjects.filter(p => p.pipelineStatus === 'Active Job').length)} />
              <StatBox label="Acceptance Rate" value={rate.pct === null ? '—' : fmtPct(rate.pct)} tone={rate.pct !== null && rate.pct < 50 ? 'red' : 'green'} />
            </div>
            {ctx.currentRole === 'Admin' && <AccountClientPortalBlock ctx={ctx} account={account} />}
            {/* Accounts have no pre-existing edit-permission gate at all (the
                nav itself is ungated) — attachments follow that same
                universal access level rather than inventing a new restriction. */}
            <AttachmentsAndActivitySection
              attachments={account.attachments} activityLog={account.activityLog}
              onAdd={data => ctx.addAccountAttachment(account.id, data)} onRemove={attId => ctx.removeAccountAttachment(account.id, attId)}
              editable={true}
            />
          </>
        )}

        {sub === 'contacts' && <AccountContactsBlock ctx={ctx} account={account} />}

        {sub === 'projects' && (
          <>
            <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)] flex-wrap">
              {['All', ...PIPELINE_STATUSES].map(s => (
                <button
                  key={s}
                  onClick={() => setFilter(s)}
                  className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold whitespace-nowrap border-b-2 ${filter === s ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}
                >
                  {s} {s !== 'All' && <span className="text-[var(--leon-black)]/30">({allProjects.filter(p => p.pipelineStatus === s).length})</span>}
                </button>
              ))}
            </div>
            {filtered.length === 0 ? <EmptyState text="No projects match." /> : (
              <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
                {filtered.map(p => <ProjectCard key={p.id} project={p} ctx={ctx} />)}
              </div>
            )}
          </>
        )}

        {sub === 'billing' && (
          ctx.canSeeFin ? (
            <>
              {(() => {
                const sales = salesValueSummary(allProjects);
                return (
                  <div className="grid sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-5">
                    <StatBox label="Contract Value" value={fmtMoney(sales.contractValue)} />
                    <StatBox label="Active Quotation Value" value={fmtMoney(sales.activeQuotationValue)} />
                    <StatBox label="Lost Quotation Value" value={fmtMoney(sales.lostQuotationValue)} tone="red" />
                    <StatBox label="Win Rate" value={sales.winRate === null ? '—' : fmtPct(sales.winRate)} tone="green" />
                    <StatBox label="Loss Rate" value={sales.lossRate === null ? '—' : fmtPct(sales.lossRate)} tone="red" />
                  </div>
                );
              })()}
              <AccountBillingPanel projects={allProjects} />
            </>
          ) : (
            <LockedNotice label="Quotations and billing figures are visible to Admin and Accounting only." />
          )
        )}
      </div>
    </div>
  );
}

function AccountBillingPanel({ projects }) {
  const totals = projects.reduce((acc, p) => {
    const prof = profitability(p);
    acc.original += prof.originalContractValue;
    acc.revised += prof.revisedContractValue;
    acc.actualCost += prof.actualCost;
    acc.collected += collectedTotal(p);
    return acc;
  }, { original: 0, revised: 0, actualCost: 0, collected: 0 });
  const allQuotes = projects.flatMap(p => p.quoteRevisions.map(q => ({ ...q, projectName: p.name })));

  return (
    <div>
      <h2 className="font-bold text-lg mb-3">Quotations &amp; Billing</h2>
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
        <StatBox label="Total Original Contract Value" value={fmtMoney(totals.original)} />
        <StatBox label="Total Revised Contract Value" value={fmtMoney(totals.revised)} />
        <StatBox label="Total Collected" value={fmtMoney(totals.collected)} />
        <StatBox label="Est. Margin" value={fmtPct(totals.revised ? ((totals.revised - totals.actualCost) / totals.revised) * 100 : 0)} />
      </div>
      <Collapsible title="Quotes Across All Projects" count={allQuotes.length}>
        {allQuotes.length === 0 ? <EmptyState text="No quotes yet." /> : (
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Project</th><th className="py-1">Rev</th><th className="py-1">Amount</th><th className="py-1">Date</th></tr></thead>
            <tbody>{allQuotes.map(q => (
              <tr key={q.id} className="border-t border-[var(--leon-line)]"><td className="py-1">{q.projectName}</td><td className="py-1">R{q.revision}</td><td className="py-1">{fmtMoney(q.amount)}</td><td className="py-1">{fmtDate(q.date)}</td></tr>
            ))}</tbody>
          </table>
        )}
      </Collapsible>
      <Collapsible title="Profitability by Project" count={projects.length}>
        <table className="w-full text-xs">
          <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Project</th><th className="py-1">Status</th><th className="py-1">Revised Value</th><th className="py-1">Actual Cost</th><th className="py-1">Margin</th></tr></thead>
          <tbody>{projects.map(p => {
            const prof = profitability(p);
            return (
              <tr key={p.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1">{p.name}</td><td className="py-1"><StatusBadge status={p.pipelineStatus} /></td>
                <td className="py-1">{fmtMoney(prof.revisedContractValue)}</td><td className="py-1">{fmtMoney(prof.actualCost)}</td><td className="py-1">{fmtPct(prof.actMarginPct)}</td>
              </tr>
            );
          })}</tbody>
        </table>
      </Collapsible>
    </div>
  );
}

// ============================================================================
// LEON Library — merges the former separate "Document Library" and
// "Material Library" nav items into one hub with subtabs, per explicit
// request. Both subtabs keep their existing components/internals unchanged,
// just mounted as subtab content instead of top-level views.
// ============================================================================
// What the letterhead calls each screen when it is printed on its own.
const VIEW_PRINT_LABELS = {
  dashboard: 'Projects', accounts: 'Accounts', accountDetail: 'Account',
  vendors: 'Vendors', vendorDetail: 'Vendor', users: 'Users', calendar: 'Calendar',
  map: 'Accounts', leonLibrary: 'LEON Library', warehouse: 'Inventory',
  logistics: 'Logistics', tradeCompliance: 'Trade Compliance', reports: 'Reports',
  accounting: 'Accounting', admin: 'LEON Collection', aboutUs: 'About Us', inbox: 'Inbox',
  leonStudio: 'LEON Studio', softwares: 'LEON Studio', office: 'LEON Studio',
  sales: 'Sales', logistics: 'Logistics', warehouse: 'Logistics',
};
const LEON_LIBRARY_SUBTABS = [
  // Company Setup used to live here; it moved to About Us, where it sits with
  // the team and the training instead of among the catalogs.
  { key: 'documents', label: 'Documents', icon: '📄' },
  { key: 'materials', label: 'Scope Documents', icon: '📑' },
  // Read-only mirror of LEON Collection -> Supplier Finishes, so everyone can
  // browse what our suppliers offer without needing Admin. Editing and
  // removing stays in LEON Collection.
  { key: 'supplierFinishes', label: 'Supplier Finishes', icon: '🎨' },
  // Read-only mirror of LEON Collection -> Render Library. Sales and the
  // associates need these boards far more often than an admin does.
  { key: 'renders', label: 'Render Library', icon: '🖼️' },
  // The portfolio — jobs we have finished and chosen to show. It follows the
  // render library because both are things you show someone.
  { key: 'finished', label: 'Finished Projects', icon: '🏆' },
  // Read-only mirror of LEON Collection -> Tariff Library. LEON Collection is
  // gated on canManageCollection, so putting the library ONLY there would have
  // meant "everyone may view it" reaching almost nobody. This is the hub the
  // whole team can open, which is where an open reference belongs.
  { key: 'tariffs', label: 'Tariff Library', icon: '🛃' },
  // Last on purpose: it is the one section you read rather than look something
  // up in, so it sits at the end of the list rather than among the catalogs.
  { key: 'training', label: 'HUB Training', icon: '🎓' },
];
// A hub's open section, held in App so the nav menu can drive and highlight it.
// Falls back to the hub's own default until someone picks one, so a hub added
// to the nav needs no migration and no extra state.
function useHubSection(ctx, hub, fallback) {
  const value = (ctx.hubSections && ctx.hubSections[hub]) || fallback;
  const set = key => ctx.setHubSection(hub, key);
  return [value, set];
}
function LeonLibraryHub({ ctx, startOn }) {
  // `startOn` covers the legacy `training` view key.
  const [sub, setSub] = useHubSection(ctx, 'leonLibrary', 'documents');
  useEffect(() => { if (startOn && startOn !== sub) setSub(startOn); }, [startOn]);
  // Same override the Collection uses: every edit control in the tariff library
  // already gates on these two capabilities, so switching them off is the whole
  // of "read-only" and nothing can be missed.
  const readOnlyCtx = useMemo(
    () => ({ ...ctx, canEditTariffClassification: false, canEditTariffShipmentInfo: false }),
    [ctx]);
  const activeSub = LEON_LIBRARY_SUBTABS.find(t => t.key === sub) || LEON_LIBRARY_SUBTABS[0];
  return (
    <div>
      <div className="mb-5">
        <h1 className="text-2xl font-bold">LEON Library</h1>
        <p className="text-sm text-[var(--leon-black)]/50">Shared company documents, the reusable material/appliance/fixture catalog, and every finish our suppliers offer &mdash; in one place.</p>
      </div>
      {/* A menu, matching LEON Collection. You come here to open ONE library and
          work in it, not to compare six across a row. */}
      <div className="flex flex-wrap items-center gap-2 mb-1 no-print">
        <label htmlFor="leon-library-section" className="text-[11px] uppercase tracking-wide font-semibold text-[var(--leon-black)]/40">Section</label>
        <Select id="leon-library-section" value={sub} onChange={e => setSub(e.target.value)} className="!w-72 !py-1.5 font-semibold">
          {LEON_LIBRARY_SUBTABS.map(t => <option key={t.key} value={t.key}>{t.icon} {t.label}</option>)}
        </Select>
      </div>
      <HubTools title={`LEON Library \u2014 ${activeSub.label}`} heading={`LEON Library \u2014 ${activeSub.label}`} />
      {sub === 'documents' && <DocumentLibraryView ctx={ctx} />}
      {sub === 'materials' && <MaterialLibraryView ctx={ctx} />}
      {sub === 'supplierFinishes' && <SupplierCatalogBrowser ctx={ctx} />}
      {sub === 'renders' && <RenderLibraryTab ctx={ctx} />}
      {sub === 'tariffs' && (
        <>
          <p className="text-sm text-[var(--leon-black)]/50 mb-4">
            Reference only &mdash; the same classifications the Export and Logistics screens read.
            Adding a code, changing a rate and recording duty are done from Logistics &rarr;
            Trade Compliance &amp; Tariffs.
          </p>
          <TariffLibrarySubTab ctx={readOnlyCtx} />
        </>
      )}
      {sub === 'finished' && <FinishedProjectsLibrary ctx={ctx} />}
      {sub === 'training' && <HubTrainingView ctx={ctx} />}
    </div>
  );
}

// HUB Training — the per-role guide, in the app rather than in a document
// nobody opens. The prose comes from training.jsx; the permission table is
// rendered LIVE from Users -> Role Permissions, so the guide cannot drift from
// what the role can actually do.
function HubTrainingView({ ctx, standalone }) {
  const [role, setRole] = useState(ctx.currentRole);
  const guide = guideForRole(role);
  const isMine = role === ctx.currentRole;

  // What this role can actually reach, straight from the live permission map.
  const modules = ALL_MODULE_KEYS
    .map(m => ({ ...m, level: roleModuleLevel(role, m.key) }))
    .filter(m => m.level !== 'none');
  const caps = CAPABILITY_DEFS.filter(c => roleHasCapability(role, c.key));

  return (
    <div>
      {standalone && <h1 className="text-2xl font-bold mb-1">HUB Training</h1>}
      <div className="flex items-start justify-between gap-3 flex-wrap mb-4">
        <p className="text-sm text-[var(--leon-black)]/50 max-w-3xl">
          How to use this system in a particular role &mdash; what the job is for, the order to do
          things in, and the traps. Everyone can read every role&rsquo;s guide; understanding what the
          person you are waiting on actually sees is usually the fastest way to unblock yourself.
        </p>
        <div className="flex items-center gap-2">
          <Select value={role} onChange={e => setRole(e.target.value)} className="!w-60">
            {SECURITY_ROLES.map(r => <option key={r} value={r}>{r}{r === ctx.currentRole ? ' — my role' : ''}</option>)}
          </Select>
          <PrintButton onClick={() => window.print()} label="Print" />
        </div>
      </div>

      {!isMine && (
        <p className="text-xs text-[var(--leon-yellow)] font-semibold mb-3">
          You are reading the {role} guide. Your own role is {ctx.currentRole}.
        </p>
      )}

      {!guide ? <EmptyState text={`No guide written for ${role} yet.`} /> : (
        <div className="space-y-4">
          <div className="border-2 border-[var(--leon-brown)] bg-[var(--leon-cream)] rounded-xl p-4">
            <p className="text-[11px] font-bold uppercase tracking-wide text-[var(--leon-brown)] mb-1">{role}</p>
            <p className="text-lg font-semibold leading-snug">{guide.mission}</p>
            {guide.sharedNote && <p className="text-xs text-[var(--leon-black)]/50 mt-2">{guide.sharedNote}</p>}
          </div>

          <Collapsible title={TRAINING_COMMON.title} count={TRAINING_COMMON.steps.length}>
            <ol className="space-y-2">
              {TRAINING_COMMON.steps.map((st, i) => (
                <li key={i} className="flex gap-3">
                  <span className="shrink-0 w-6 h-6 rounded-full bg-[var(--leon-brown)] text-white text-xs font-bold grid place-items-center">{i + 1}</span>
                  <span><b className="text-sm">{st.t}</b><span className="block text-sm text-[var(--leon-black)]/60">{st.d}</span></span>
                </li>
              ))}
            </ol>
          </Collapsible>

          {guide.startHere.length > 0 && (
            <Collapsible title="Start here" count={guide.startHere.length} defaultOpen>
              <ol className="space-y-1.5">
                {guide.startHere.map((t, i) => (
                  <li key={i} className="flex gap-3 text-sm">
                    <span className="shrink-0 w-6 h-6 rounded-full bg-[var(--leon-brown)] text-white text-xs font-bold grid place-items-center">{i + 1}</span>
                    <span>{t}</span>
                  </li>
                ))}
              </ol>
            </Collapsible>
          )}

          {guide.daily.length > 0 && (
            <Collapsible title="Your day" count={guide.daily.length}>
              <ul className="space-y-1 text-sm list-disc pl-5">{guide.daily.map((t, i) => <li key={i}>{t}</li>)}</ul>
            </Collapsible>
          )}

          <Collapsible title="How to do the work" count={guide.tasks.length} defaultOpen>
            <div className="space-y-3">
              {guide.tasks.map((task, i) => (
                <div key={i} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-baseline gap-2 flex-wrap mb-1.5">
                    <p className="font-bold text-sm">{task.title}</p>
                    {task.when && <span className="text-[11px] text-[var(--leon-black)]/45">when: {task.when}</span>}
                  </div>
                  <ol className="space-y-1 text-sm list-decimal pl-5">{task.steps.map((st, j) => <li key={j}>{st}</li>)}</ol>
                  {task.note && (
                    <p className="mt-2 text-xs bg-[var(--leon-yellow)]/10 border-l-2 border-[var(--leon-yellow)] pl-2 py-1.5 text-[var(--leon-black)]/70">{task.note}</p>
                  )}
                </div>
              ))}
            </div>
          </Collapsible>

          {/* Generated, not written — see the note at the top of training.jsx. */}
          <Collapsible title="What this role can reach" count={modules.length}>
            <p className="text-xs text-[var(--leon-black)]/45 mb-2">
              Read live from Users &rarr; Role Permissions. If an admin changes the role, this changes with it.
            </p>
            <div className="flex flex-wrap gap-1.5 mb-3">
              {modules.map(m => (
                <span key={m.key} className={`px-2 py-1 rounded text-[11px] font-semibold border ${m.level === 'edit' ? 'border-[var(--leon-brown)] text-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/50'}`}>
                  {m.label}<span className="ml-1 opacity-60">{m.level === 'edit' ? 'edit' : 'view'}</span>
                </span>
              ))}
            </div>
            {caps.length > 0 && (
              <>
                <p className="text-[11px] font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1">Can also</p>
                <ul className="text-sm list-disc pl-5">{caps.map(c => <li key={c.key}>{c.label}</li>)}</ul>
              </>
            )}
          </Collapsible>

          <Collapsible title="Where your job stops" count={guide.boundaries.length}>
            <ul className="space-y-1 text-sm list-disc pl-5">{guide.boundaries.map((t, i) => <li key={i}>{t}</li>)}</ul>
            {guide.escalate.length > 0 && (
              <>
                <p className="text-[11px] font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mt-3 mb-1">Who to ask</p>
                <ul className="space-y-1 text-sm list-disc pl-5">{guide.escalate.map((t, i) => <li key={i}>{t}</li>)}</ul>
              </>
            )}
          </Collapsible>

          {/* A recording script, not a recording — see the note in the panel. */}
          {guide.video && (
            <Collapsible title={`Training video script — ${guide.video.title}`}>
              <p className="text-xs text-[var(--leon-black)]/50 mb-2">
                Roughly {guide.video.minutes} minutes. This is the shot list for someone to record with
                screen capture &mdash; the system cannot produce video itself. Record it against the demo
                scenario (LEON Collection &rarr; Demo Data) so no real client data is on screen.
              </p>
              <ol className="space-y-1 text-sm list-decimal pl-5">{guide.video.beats.map((b, i) => <li key={i}>{b}</li>)}</ol>
            </Collapsible>
          )}
        </div>
      )}
    </div>
  );
}

// ============================================================================
// About Us — who LEON is, who works here, and how to use this system
// ============================================================================
// Company Setup, Meet the Team, Users and the role training guides were four
// unrelated places to look for "things about the company". They are one hub now,
// which is also the only sensible home for the HUB Training material.
const ABOUT_US_SUBTABS = [
  { key: 'company', label: 'Company Profile', icon: '🏢' },
  { key: 'team', label: 'Meet the Team', icon: '👥' },
  // Subcontractors are not employees, but they are who the work is built with,
  // so they belong on the page the whole company reads rather than only inside
  // the Vendors record that carries their rates and insurance.
  { key: 'subs', label: 'Subcontractors', icon: '🔧' },
  { key: 'orgChart', label: 'Organization Chart', icon: '🗂️' },
];
function AboutUsHub({ ctx }) {
  const [sub, setSub] = useHubSection(ctx, 'aboutUs', 'company');
  const tabs = ABOUT_US_SUBTABS;
  return (
    <div>
      <div className="mb-5">
        <h1 className="text-2xl font-bold">About Us</h1>
        <p className="text-sm text-[var(--leon-black)]/50">
          LEON&rsquo;s own record, everyone who works here, and the guide to using this system in your role.
        </p>
      </div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}
          </button>
        ))}
      </div>
      <HubTools />
      {sub === 'company' && <CompanySetupView ctx={ctx} />}
      {sub === 'team' && <MeetTheTeamView ctx={ctx} embedded />}
      {sub === 'subs' && <MeetTheSubcontractors ctx={ctx} />}
      {sub === 'orgChart' && <OrgChartView ctx={ctx} />}
    </div>
  );
}

// ============================================================================
// Document Library — shared company resources for all employees
// ============================================================================
function DocumentLibraryView({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [filter, setFilter] = useState('All');
  // The screen had NO gate: + Add Document, the remove control and the file
  // field were all unconditional, so anyone who could reach the page could
  // delete the company's policies. Reading is deliberately still open to
  // everyone — this is a shared reference shelf — but changing it is not.
  const mayEdit = ctx.canEditDocLibrary !== false;
  const grouped = DOCUMENT_LIBRARY_CATEGORIES.map(cat => ({
    cat,
    items: ctx.documentLibrary.filter(d => d.category === cat && (filter === 'All' || filter === cat)),
  })).filter(g => g.items.length > 0 || filter === g.cat);

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-3">
        <p className="text-sm text-[var(--leon-black)]/50">Shared reference documents available to every employee — policies, templates, standards, and forms.</p>
        {mayEdit && <Button onClick={() => setShowAdd(true)}>+ Add Document</Button>}
      </div>

      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {['All', ...DOCUMENT_LIBRARY_CATEGORIES].map(c => (
          <button key={c} onClick={() => setFilter(c)} className={`px-3 py-2 text-sm font-semibold whitespace-nowrap border-b-2 ${filter === c ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{c}</button>
        ))}
      </div>
      <HubTools />

      {ctx.documentLibrary.length === 0 ? <EmptyState text="No documents in the library yet." /> : (
        DOCUMENT_LIBRARY_CATEGORIES.filter(cat => filter === 'All' || filter === cat).map(cat => {
          const items = ctx.documentLibrary.filter(d => d.category === cat);
          if (items.length === 0) return null;
          return (
            <Collapsible key={cat} title={cat} count={items.length}>
              <div className="space-y-1.5">
                {items.map(d => (
                  <div key={d.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
                    <div className="min-w-0">
                      <p className="text-sm font-semibold truncate">{d.name}</p>
                      <p className="text-xs text-[var(--leon-black)]/50 flex items-center gap-1.5 flex-wrap">
                        <FileField name={d.file} url={d.fileUrl} onChange={(fname, url) => ctx.updateLibraryDocument(d.id, { file: fname, fileUrl: url })} editable={mayEdit} />
                        · uploaded by {d.uploadedBy} · {fmtDate(d.date)}{d.note ? ` — ${d.note}` : ''}
                      </p>
                    </div>
                    {mayEdit && <IconBtn title="Remove" onClick={() => ctx.removeLibraryDocument(d.id)}>✕</IconBtn>}
                  </div>
                ))}
              </div>
            </Collapsible>
          );
        })
      )}

      <AddLibraryDocumentModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
    </div>
  );
}
function AddLibraryDocumentModal({ open, onClose, ctx }) {
  const [form, setForm] = useState({ name: '', category: DOCUMENT_LIBRARY_CATEGORIES[0], file: '', fileUrl: null, note: '' });
  useEffect(() => { if (open) setForm({ name: '', category: DOCUMENT_LIBRARY_CATEGORIES[0], file: '', fileUrl: null, note: '' }); }, [open]);
  function submit() {
    if (!form.name.trim()) return;
    ctx.addLibraryDocument(form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Add Document to Library" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Document</Button></>}>
      <div className="space-y-3">
        <Field label="Document Title"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>{DOCUMENT_LIBRARY_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Note (optional)"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ============================================================================
// Material Specification Library (§2, §4, §14-22) — a company-wide, reusable
// catalog of materials/products with structured specs and reusable documents
// (Tech Data Sheet, Care & Maintenance, Warranty, Installation, Certs). Once
// uploaded here, a material's documents don't need to be re-uploaded per
// project — Selections link to a material by id instead.
// ============================================================================
function SpecFieldInput({ def, value, onChange }) {
  if (!def.options) return <TextInput value={value || ''} onChange={e => onChange(e.target.value)} placeholder={def.label} />;
  const [customMode, setCustomMode] = useState(!!value && !def.options.includes(value));
  return customMode ? (
    <div className="flex items-center gap-1">
      <TextInput value={value || ''} onChange={e => onChange(e.target.value)} placeholder="Custom value" autoFocus />
      <IconBtn title="Back to list" onClick={() => { setCustomMode(false); onChange(''); }}>✕</IconBtn>
    </div>
  ) : (
    <Select value={value || ''} onChange={e => { if (e.target.value === '__custom__') { setCustomMode(true); onChange(''); } else onChange(e.target.value); }}>
      <option value="">—</option>
      {def.options.map(o => <option key={o} value={o}>{o}</option>)}
      <option value="__custom__">Custom…</option>
    </Select>
  );
}
const LIBRARY_SUBTABS = [
  { key: 'materials', label: 'Scope Documents', icon: '📑' },
  { key: 'appliances', label: 'Appliances', icon: '🔌' },
  { key: 'fixtures', label: 'Fixtures', icon: '🚿' },
];
function MaterialLibraryView({ ctx }) {
  const [sub, setSub] = useState('materials');
  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3">Reusable material, appliance, and fixture specs — stored once and linked from any project.</p>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)]">
        {LIBRARY_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools title="Material Library" heading="Material Library" />
      {sub === 'materials' && <MaterialsSubTab ctx={ctx} editable={false} />}
      {sub === 'appliances' && <SpecLibrarySubTab ctx={ctx} kind="appliance" />}
      {sub === 'fixtures' && <SpecLibrarySubTab ctx={ctx} kind="fixture" />}
    </div>
  );
}
// Supporting documents for a scope — the paperwork behind the material, not a
// second copy of the material. The material itself lives in Supplier Finishes,
// with the supplier's own catalog behind it; what that cannot hold is the
// technical data sheet, the care instructions, the warranty and the install
// guide. Each document is filed against a scope FAMILY and optionally one
// CATEGORY of it, so the right sheet surfaces where the choice is made.
function MaterialsSubTab({ ctx, editable }) {
  const canEdit = editable !== false;
  const [showAdd, setShowAdd] = useState(false);
  const [edit, setEdit] = useState(null);
  const [family, setFamily] = useState('all');
  const [docType, setDocType] = useState('all');
  const [search, setSearch] = useState('');
  const families = ctx.scopeLibrary.filter(f => f.active);
  const docs = (ctx.scopeDocuments || []).filter(d => d.active !== false);
  const ql = search.trim().toLowerCase();
  const shown = docs.filter(d =>
    (family === 'all' || d.familyName === family)
    && (docType === 'all' || d.docType === docType)
    && (!ql || `${d.name} ${d.notes} ${d.docType}`.toLowerCase().includes(ql)));
  // Grouped the way they are filed: family, then the category within it.
  const groups = families
    .map(f => ({ fam: f, items: shown.filter(d => d.familyName === f.name) }))
    .filter(g => g.items.length);
  const orphans = shown.filter(d => !families.some(f => f.name === d.familyName));

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3 max-w-3xl">
        The paperwork behind a scope &mdash; technical data sheets, care and cleaning instructions,
        warranties and installation guides. Filed against a scope family, and against one of its
        selection categories where it belongs to just that choice, so it surfaces in the Selection Hub
        beside the finish it documents. <b>The materials themselves live in Supplier Finishes.</b>
      </p>
      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput placeholder="Search documents…" value={search} onChange={e => setSearch(e.target.value)} className="!w-52" />
        <Select value={family} onChange={e => setFamily(e.target.value)} className="!w-52 !py-1 !text-xs">
          <option value="all">All scope families</option>
          {families.map(f => <option key={f.id} value={f.name}>{f.name}</option>)}
        </Select>
        <Select value={docType} onChange={e => setDocType(e.target.value)} className="!w-56 !py-1 !text-xs">
          <option value="all">All document types</option>
          {MATERIAL_DOC_TYPES.map(t => <option key={t}>{t}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45">{shown.length} of {docs.length}</span>
        <div className="flex-1" />
        {canEdit && <Button onClick={() => { setEdit(null); setShowAdd(true); }}>+ Add Document</Button>}
      </div>
      {shown.length === 0 ? (
        <EmptyState text={docs.length ? 'Nothing matches those filters.' : 'No supporting documents yet. Add the spec sheets, warranties and care instructions your team gets asked for.'} />
      ) : (
        <div className="space-y-3">
          {[...groups, ...(orphans.length ? [{ fam: { id: '__other', name: 'Not in the scope library' }, items: orphans }] : [])].map(g => {
            const cats = g.fam.categories || [];
            const byCat = [
              { id: null, name: 'Applies to the whole family', items: g.items.filter(d => !d.categoryId) },
              ...cats.map(c => ({ id: c.id, name: c.name, items: g.items.filter(d => d.categoryId === c.id) })),
            ].filter(x => x.items.length);
            return (
              <div key={g.fam.id} className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
                <div className="px-3 py-2 bg-[var(--leon-cream)] flex items-center gap-2">
                  <span className="text-sm font-bold">{g.fam.name}</span>
                  <span className="text-xs text-[var(--leon-black)]/45">{g.items.length} document{g.items.length === 1 ? '' : 's'}</span>
                </div>
                {byCat.map(c => (
                  <div key={c.id || 'family'} className="border-t border-[var(--leon-line)] first:border-0">
                    <p className="px-3 pt-2 text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/40">{c.name}</p>
                    <div className="divide-y divide-[var(--leon-line)]">
                      {c.items.map(d => (
                        <div key={d.id} className="flex items-center gap-3 px-3 py-2 text-xs">
                          <Badge tone="neutral">{d.docType}</Badge>
                          <span className="flex-1 min-w-0">
                            <span className="font-semibold">{d.name}</span>
                            {d.notes && <span className="text-[var(--leon-black)]/45"> · {d.notes}</span>}
                          </span>
                          {d.vendorId && <span className="text-[var(--leon-black)]/40 shrink-0">{(ctx.vendors.find(v => v.id === d.vendorId) || {}).name}</span>}
                          <AttachmentLink name={d.file || d.name} url={d.fileUrl} />
                          {canEdit && <>
                            <button onClick={() => { setEdit(d); setShowAdd(true); }} className="text-[var(--leon-brown)] font-semibold shrink-0">Edit</button>
                            <IconBtn title="Remove this document" onClick={() => { if (confirm(`Remove "${d.name}"?`)) ctx.removeScopeDocument(d.id); }}>&#10005;</IconBtn>
                          </>}
                        </div>
                      ))}
                    </div>
                  </div>
                ))}
              </div>
            );
          })}
        </div>
      )}
      <ScopeDocumentModal open={showAdd} doc={edit} ctx={ctx} onClose={() => { setShowAdd(false); setEdit(null); }} />
    </div>
  );
}

function ScopeDocumentModal({ open, doc, ctx, onClose }) {
  const blank = { familyName: '', categoryId: '', docType: MATERIAL_DOC_TYPES[0], name: '', file: null, fileUrl: null, vendorId: '', notes: '' };
  const [f, setF] = useState(blank);
  useEffect(() => {
    if (!open) return;
    setF(doc ? { ...blank, ...doc, categoryId: doc.categoryId || '', vendorId: doc.vendorId || '' }
      : { ...blank, familyName: (ctx.scopeLibrary.find(x => x.active) || {}).name || '' });
  }, [open, doc && doc.id]);
  const fam = ctx.scopeLibrary.find(x => x.name === f.familyName);
  const cats = (fam && fam.categories) || [];
  const ready = f.familyName && f.name.trim() && f.docType;
  function submit() {
    const payload = { ...f, name: f.name.trim(), categoryId: f.categoryId || null, vendorId: f.vendorId || null };
    if (doc) ctx.updateScopeDocument(doc.id, payload); else ctx.addScopeDocument(payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={doc ? 'Edit document' : 'Add a supporting document'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!ready}>{doc ? 'Save' : 'Add document'}</Button></>}>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Scope family">
            <Select value={f.familyName} onChange={e => setF({ ...f, familyName: e.target.value, categoryId: '' })}>
              <option value="">— choose a family —</option>
              {ctx.scopeLibrary.filter(x => x.active).map(x => <option key={x.id} value={x.name}>{x.name}</option>)}
            </Select>
          </Field>
          <Field label="Category" hint="Leave blank when it applies to the whole family.">
            <Select value={f.categoryId} onChange={e => setF({ ...f, categoryId: e.target.value })} disabled={!cats.length}>
              <option value="">— applies to the whole family —</option>
              {cats.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Document type">
            <Select value={f.docType} onChange={e => setF({ ...f, docType: e.target.value })}>
              {MATERIAL_DOC_TYPES.map(t => <option key={t}>{t}</option>)}
            </Select>
          </Field>
          <Field label="Vendor (optional)" hint="Whose product this documents.">
            <Select value={f.vendorId} onChange={e => setF({ ...f, vendorId: e.target.value })}>
              <option value="">— not vendor-specific —</option>
              {ctx.vendors.filter(v => v.status !== 'Inactive').map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Title" hint="What someone would look for it by.">
          <TextInput value={f.name} onChange={e => setF({ ...f, name: e.target.value })} placeholder="e.g. Engineered oak — care & cleaning" />
        </Field>
        <Field label="File">
          <FileField name={f.file} url={f.fileUrl} editable placeholder="Upload the document"
            onChange={(name, url) => setF({ ...f, file: name, fileUrl: url })} />
        </Field>
        <Field label="Notes (optional)">
          <TextInput value={f.notes} onChange={e => setF({ ...f, notes: e.target.value })} placeholder="e.g. applies to the 7.5in plank only" />
        </Field>
      </div>
    </Modal>
  );
}

// The documents filed for one selection category, shown where the choice is
// made. Read-only here on purpose — the library is edited in LEON Collection,
// and a spec sheet is not a per-project thing.
function CategoryDocsLink({ ctx, familyName, categoryId }) {
  const [open, setOpen] = useState(false);
  const docs = scopeDocumentsFor(ctx.scopeDocuments, familyName, categoryId);
  if (!docs.length) return null;
  return (
    <span className="relative">
      <button type="button" onClick={() => setOpen(o => !o)}
        className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap"
        title="Spec sheets, warranties and care instructions on file for this category">
        📄 {docs.length} doc{docs.length === 1 ? '' : 's'}
      </button>
      {open && (
        <span className="absolute z-20 left-0 top-6 w-80 bg-white border border-[var(--leon-line)] rounded-lg shadow-lg p-2 block">
          {docs.map(d => (
            <span key={d.id} className="flex items-center gap-2 py-1 text-xs border-b border-[var(--leon-line)] last:border-0">
              <Badge tone="neutral">{d.docType}</Badge>
              <span className="flex-1 min-w-0 truncate" title={d.name}>{d.name}</span>
              <AttachmentLink name={d.file || d.name} url={d.fileUrl} />
            </span>
          ))}
        </span>
      )}
    </span>
  );
}

function SpecLibrarySubTab({ ctx, kind }) {
  const isAppliance = kind === 'appliance';
  const library = isAppliance ? ctx.applianceLibrary : ctx.fixtureLibrary;
  const types = isAppliance ? APPLIANCE_TYPES : FIXTURE_TYPES;
  const [showAdd, setShowAdd] = useState(false);
  const [filter, setFilter] = useState('All');
  const [search, setSearch] = useState('');
  const filtered = library.filter(s =>
    (filter === 'All' || (isAppliance ? s.applianceType : s.fixtureType) === filter) &&
    (!search || s.model.toLowerCase().includes(search.toLowerCase()) || (s.manufacturer || '').toLowerCase().includes(search.toLowerCase()))
  );
  return (
    <div>
      <div className="flex items-center justify-end gap-2 mb-3">
        <TextInput placeholder={`Search ${isAppliance ? 'appliances' : 'fixtures'}…`} value={search} onChange={e => setSearch(e.target.value)} className="!w-52" />
        <Button onClick={() => setShowAdd(true)}>+ Add {isAppliance ? 'Appliance' : 'Fixture'}</Button>
      </div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {['All', ...types].map(c => (
          <button key={c} onClick={() => setFilter(c)} className={`px-3 py-2 text-sm font-semibold whitespace-nowrap border-b-2 ${filter === c ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{c}</button>
        ))}
      </div>
      <HubTools />
      {filtered.length === 0 ? <EmptyState text={`No ${isAppliance ? 'appliance' : 'fixture'} specs in the library yet.`} /> : (
        <div className="space-y-2">
          {filtered.map(s => (
            <div key={s.id} className="border border-[var(--leon-line)] rounded-lg p-3">
              <div className="flex items-center justify-between gap-2 flex-wrap">
                <div>
                  <p className="text-sm font-bold">{isAppliance ? s.applianceType : s.fixtureType} — {s.manufacturer} {s.model}</p>
                  <p className="text-xs text-[var(--leon-black)]/50">{s.modelNumber ? `Model #${s.modelNumber} · ` : ''}{s.finish}{s.dimensions ? ` · ${s.dimensions}` : ''}</p>
                </div>
                {!s.active && <Badge tone="neutral">Inactive</Badge>}
              </div>
              {s.documents.length > 0 && (
                <div className="flex flex-wrap gap-2 mt-1.5">
                  {s.documents.map(d => <FileField key={d.id} name={`${d.docType}: ${d.name}`} url={d.fileUrl} editable={false} onChange={() => {}} />)}
                </div>
              )}
            </div>
          ))}
        </div>
      )}
      <AddLibrarySpecModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} kind={kind} />
    </div>
  );
}
function AddLibrarySpecModal({ open, onClose, ctx, kind }) {
  const isAppliance = kind === 'appliance';
  const types = isAppliance ? APPLIANCE_TYPES : FIXTURE_TYPES;
  const blank = isAppliance
    ? { applianceType: types[0], manufacturer: '', model: '', modelNumber: '', finish: '', dimensions: '', voltage: '', gasRequirement: '', plumbingRequirement: '', ventilationRequirement: '', notes: '' }
    : { fixtureType: types[0], manufacturer: '', model: '', modelNumber: '', finish: '', dimensions: '', mountingType: '', plumbingRequirements: '', cutoutDimensions: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [docs, setDocs] = useState({});
  useEffect(() => { if (open) { setForm(blank); setDocs({}); } }, [open]);
  function submit() {
    if (!form.manufacturer.trim() || !form.model.trim()) return;
    const created = isAppliance ? ctx.addApplianceSpec(form) : ctx.addFixtureSpec(form);
    SPEC_DOC_TYPES.forEach(docType => {
      const d = docs[docType];
      if (d && d.fileUrl) {
        if (isAppliance) ctx.addApplianceSpecDocument(created.id, docType, d.file, d.fileUrl);
        else ctx.addFixtureSpecDocument(created.id, docType, d.file, d.fileUrl);
      }
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Add ${isAppliance ? 'Appliance' : 'Fixture'} to Library`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add to Library</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label={isAppliance ? 'Appliance Type' : 'Fixture Type'}>
            <Select value={isAppliance ? form.applianceType : form.fixtureType} onChange={e => setForm({ ...form, [isAppliance ? 'applianceType' : 'fixtureType']: e.target.value })}>
              {types.map(t => <option key={t}>{t}</option>)}
            </Select>
          </Field>
          <Field label="Manufacturer"><TextInput value={form.manufacturer} onChange={e => setForm({ ...form, manufacturer: e.target.value })} /></Field>
          <Field label="Model"><TextInput value={form.model} onChange={e => setForm({ ...form, model: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Model Number"><TextInput value={form.modelNumber} onChange={e => setForm({ ...form, modelNumber: e.target.value })} /></Field>
          <Field label="Finish / Color"><TextInput value={form.finish} onChange={e => setForm({ ...form, finish: e.target.value })} /></Field>
          <Field label="Dimensions"><TextInput value={form.dimensions} onChange={e => setForm({ ...form, dimensions: e.target.value })} /></Field>
        </div>
        {isAppliance ? (
          <div className="grid grid-cols-2 gap-3">
            <Field label="Voltage / Electrical"><TextInput value={form.voltage} onChange={e => setForm({ ...form, voltage: e.target.value })} /></Field>
            <Field label="Gas Requirement"><TextInput value={form.gasRequirement} onChange={e => setForm({ ...form, gasRequirement: e.target.value })} /></Field>
            <Field label="Plumbing Requirement"><TextInput value={form.plumbingRequirement} onChange={e => setForm({ ...form, plumbingRequirement: e.target.value })} /></Field>
            <Field label="Ventilation Requirement"><TextInput value={form.ventilationRequirement} onChange={e => setForm({ ...form, ventilationRequirement: e.target.value })} /></Field>
          </div>
        ) : (
          <div className="grid grid-cols-3 gap-3">
            <Field label="Mounting Type"><TextInput value={form.mountingType} onChange={e => setForm({ ...form, mountingType: e.target.value })} /></Field>
            <Field label="Plumbing Requirements"><TextInput value={form.plumbingRequirements} onChange={e => setForm({ ...form, plumbingRequirements: e.target.value })} /></Field>
            <Field label="Cutout Dimensions"><TextInput value={form.cutoutDimensions} onChange={e => setForm({ ...form, cutoutDimensions: e.target.value })} /></Field>
          </div>
        )}
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          {SPEC_DOC_TYPES.map(docType => (
            <Field key={docType} label={docType}>
              <FileField name={(docs[docType] || {}).file} url={(docs[docType] || {}).fileUrl} editable onChange={(fname, url) => setDocs(d => ({ ...d, [docType]: { file: fname, fileUrl: url } }))} />
            </Field>
          ))}
        </div>
      </div>
    </Modal>
  );
}
function MaterialCard({ ctx, material }) {
  const [docModal, setDocModal] = useState(false);
  const fieldDefs = MATERIAL_FIELD_DEFS[material.category] || [];
  return (
    <Collapsible
      title={material.name}
      count={material.documents.length}
      right={
        <div className="flex items-center gap-2">
          <Badge tone="neutral">{material.category}</Badge>
          {!material.active && <Badge tone="neutral">Inactive</Badge>}
        </div>
      }
    >
      <div className="grid md:grid-cols-2 gap-4">
        <div>
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">{material.manufacturer}{material.vendorName ? ` · ${material.vendorName}` : ''}{material.productCode ? ` · #${material.productCode}` : ''}</p>
          <div className="space-y-1 text-xs">
            {fieldDefs.filter(f => material.specs[f.key]).map(f => (
              <div key={f.key} className="flex justify-between border-b border-[var(--leon-line)] py-1">
                <span className="text-[var(--leon-black)]/50">{f.label}</span>
                <span className="font-semibold">{material.specs[f.key]}</span>
              </div>
            ))}
          </div>
          {material.notes && <p className="text-xs text-[var(--leon-black)]/50 mt-2 italic">{material.notes}</p>}
          <Button size="sm" variant="ghost" className="mt-2" onClick={() => ctx.setMaterialActive(material.id, !material.active)}>{material.active ? 'Deactivate' : 'Activate'}</Button>
        </div>
        <div>
          <div className="flex items-center justify-between mb-1.5">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Documents</p>
            <Button size="sm" variant="ghost" onClick={() => setDocModal(true)}>+ Add Document</Button>
          </div>
          {material.documents.length === 0 ? <EmptyState text="No documents yet." /> : (
            <div className="space-y-1">
              {material.documents.map(d => (
                <div key={d.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-md px-2 py-1.5">
                  <div className="min-w-0 text-xs">
                    <Badge tone="neutral">{d.docType}</Badge>
                    <div className="mt-0.5"><FileField name={d.file} url={d.fileUrl} editable={false} onChange={() => {}} /></div>
                  </div>
                  <IconBtn title="Remove" onClick={() => ctx.removeMaterialDocument(material.id, d.id)}>✕</IconBtn>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
      <AddMaterialDocumentModal open={docModal} onClose={() => setDocModal(false)} ctx={ctx} material={material} />
    </Collapsible>
  );
}
function AddMaterialModal({ open, onClose, ctx }) {
  const blank = { name: '', category: MATERIAL_CATEGORIES[0], manufacturer: '', vendorId: '', productCode: '', unit: 'Imperial', notes: '', specs: {} };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  const fieldDefs = MATERIAL_FIELD_DEFS[form.category] || [];
  function setSpec(key, value) { setForm(f => ({ ...f, specs: { ...f.specs, [key]: value } })); }
  function submit() {
    if (!form.name.trim()) return;
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    ctx.addMaterial({ ...form, vendorName: vendor ? vendor.name : '' });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title="Add Material" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Material</Button></>}>
      <div className="space-y-3">
        <Field label="Material Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. European White Oak Engineered Flooring" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value, specs: {} })}>{MATERIAL_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
          <Field label="Unit"><Select value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })}>{DIMENSION_UNITS.map(u => <option key={u}>{u}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Manufacturer"><TextInput value={form.manufacturer} onChange={e => setForm({ ...form, manufacturer: e.target.value })} /></Field>
          <Field label="Vendor"><Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}><option value="">—</option>{ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</Select></Field>
        </div>
        <Field label="Product Code"><TextInput value={form.productCode} onChange={e => setForm({ ...form, productCode: e.target.value })} /></Field>
        {fieldDefs.length > 0 && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 grid grid-cols-2 gap-3">
            {fieldDefs.map(f => (
              <Field key={f.key} label={f.label}><SpecFieldInput def={f} value={form.specs[f.key]} onChange={v => setSpec(f.key, v)} /></Field>
            ))}
          </div>
        )}
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddMaterialDocumentModal({ open, onClose, ctx, material }) {
  const blank = { docType: MATERIAL_DOC_TYPES[0], file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.file) return;
    ctx.addMaterialDocument(material.id, form.docType, form.file, form.file, form.fileUrl);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Document — ${material.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Document Type"><Select value={form.docType} onChange={e => setForm({ ...form, docType: e.target.value })}>{MATERIAL_DOC_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
      </div>
    </Modal>
  );
}

// ============================================================================
// Logistics Dashboard — the operational control center connecting
// Procurement → Export → Shipment → Customs → Warehouse → Allocation →
// Delivery → Project Cost. Reads live from the top-level ctx.exportContainers,
// project.deliveries, ctx.warehouseMaterials/materialAllocations, and the
// top-level logisticsClaims — no parallel data store. The full
// searchable/sortable/filterable/exportable report views for each of these
// live under Reports → Logistics Reports (reports.jsx); this view is the
// at-a-glance KPI/action surface, same relationship My To-Do has to the
// per-project Tasks tab.
// ============================================================================
const LOGISTICS_HUB_SUBTABS = [
  { key: 'dashboard', label: 'Dashboard', icon: '📊' },
  // Stock is something Logistics holds. It used to be its own nav destination,
  // which put the warehouse a click further from the deliveries it feeds.
  { key: 'inventory', label: 'Inventory', icon: '📦' },
  { key: 'drivers', label: 'Driver Management', icon: '🧑‍✈️' },
  { key: 'trucks', label: 'Trucks', icon: '🚛' },
];
function LogisticsDashboard({ ctx, startOn }) {
  // Stock rights and logistics rights are different things: a warehouse
  // assistant may hold the first and not the second. Rather than locking the
  // whole hub, only the subtabs they cannot see are withheld.
  const canStock = !!(ctx.canAllocateMaterial || ctx.canAssistWarehouse || ctx.canCreateInventory);
  const tabs = LOGISTICS_HUB_SUBTABS.filter(t =>
    t.key === 'inventory' ? canStock : ctx.canSeeLogistics);
  const [sub, setSub] = useHubSection(ctx, 'logistics', startOn || (tabs[0] && tabs[0].key) || 'dashboard');
  useEffect(() => { if (startOn && startOn !== sub) setSub(startOn); }, [startOn]);
  if (!tabs.length) return <LockedNotice />;
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />
      {sub === 'dashboard' && <LogisticsDashboardHome ctx={ctx} />}
      {sub === 'inventory' && <WarehouseHubTab ctx={ctx} embedded />}
      {sub === 'drivers' && <DriverManagementSubTab ctx={ctx} />}
      {sub === 'trucks' && <TruckManagementSubTab ctx={ctx} />}
    </div>
  );
}
// Basic add/edit for Delivery Driver accounts (Phase 8) — reuses
// AddUserModal's own flow, fixed to that one role.
function DriverManagementSubTab({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [passwordFor, setPasswordFor] = useState(null);
  const drivers = ctx.teamDirectory.filter(p => p.securityRole === 'Delivery Driver');
  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowAdd(true)}>+ Add Driver</Button></div>
      <div className="bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden overflow-x-auto">
        <table className="w-full text-sm">
          <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-xs uppercase text-[var(--leon-black)]/50"><th className="px-4 py-2">Name</th><th className="px-4 py-2">Contact</th><th className="px-4 py-2">Status</th><th className="px-4 py-2"></th></tr></thead>
          <tbody>
            {drivers.length === 0 ? (
              <tr><td colSpan={4} className="px-4 py-6"><EmptyState text="No delivery drivers yet." /></td></tr>
            ) : drivers.map(p => (
              <tr key={p.id} className="border-t border-[var(--leon-line)]">
                <td className="px-4 py-2 font-semibold flex items-center gap-2 whitespace-nowrap"><Avatar name={p.name} url={p.photoUrl} size={24} />{p.name}</td>
                <td className="px-4 py-2 text-[var(--leon-black)]/60">
                  <TextInput value={p.email || ''} onChange={e => ctx.updateUser(p.id, { email: e.target.value })} className="!py-1 !text-xs !mb-1" placeholder="Email" />
                  <TextInput value={p.phone || ''} onChange={e => ctx.updateUser(p.id, { phone: e.target.value })} className="!py-1 !text-xs" placeholder="Phone" />
                </td>
                <td className="px-4 py-2">{p.active ? <Badge tone="green">Active</Badge> : <Badge tone="neutral">Inactive</Badge>}</td>
                <td className="px-4 py-2 whitespace-nowrap">
                  <Button size="sm" variant="ghost" onClick={() => setPasswordFor(p)}>Reset password</Button>
                  <Button size="sm" variant="ghost" onClick={() => ctx.setUserActive(p.id, !p.active)}>{p.active ? 'Deactivate' : 'Activate'}</Button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <AddUserModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} fixedRole="Delivery Driver" />
      <SetPasswordModal user={passwordFor} onClose={() => setPasswordFor(null)} ctx={ctx} />
    </div>
  );
}
// Truck/vehicle maintenance (Phase 8) — a maintenance window on a truck
// blocks the Delivery Calendar/scheduling flow for whichever driver is
// normally assigned to it (checked in ScheduleDeliveryModal).
function TruckManagementSubTab({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [editFor, setEditFor] = useState(null);
  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowAdd(true)}>+ Add Truck</Button></div>
      <Collapsible title="Trucks" count={ctx.trucks.length}>
        {ctx.trucks.length === 0 ? <EmptyState text="No trucks on file yet." /> : (
          <div className="space-y-2">
            {ctx.trucks.map(t => {
              const driver = ctx.teamDirectory.find(p => p.id === t.assignedDriverId);
              const underMaintenance = truckUnderMaintenanceOn(t, todayISO());
              return (
                <div key={t.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-center justify-between gap-2 flex-wrap">
                    <div>
                      <p className="text-sm font-bold">{t.name}{t.plateNumber ? ` — ${t.plateNumber}` : ''}</p>
                      <p className="text-xs text-[var(--leon-black)]/50">Assigned Driver: {driver ? driver.name : 'Unassigned'}</p>
                    </div>
                    <div className="flex items-center gap-2">
                      {underMaintenance && <Badge tone="red">Under Maintenance</Badge>}
                      <Button size="sm" variant="ghost" onClick={() => setEditFor(t)}>✎ Edit</Button>
                    </div>
                  </div>
                  {t.maintenanceStart && (
                    <p className="text-xs text-[var(--leon-black)]/50 mt-1">Maintenance window: {fmtDate(t.maintenanceStart)} – {fmtDate(t.maintenanceEnd || t.maintenanceStart)}{t.maintenanceReason ? ` — ${t.maintenanceReason}` : ''}</p>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddEditTruckModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
      <AddEditTruckModal open={!!editFor} truck={editFor} onClose={() => setEditFor(null)} ctx={ctx} />
    </div>
  );
}
function AddEditTruckModal({ open, truck, onClose, ctx }) {
  const isEdit = !!truck;
  const blank = { name: '', plateNumber: '', assignedDriverId: '', maintenanceStart: '', maintenanceEnd: '', maintenanceReason: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (truck) setForm({ name: truck.name, plateNumber: truck.plateNumber, assignedDriverId: truck.assignedDriverId || '', maintenanceStart: truck.maintenanceStart || '', maintenanceEnd: truck.maintenanceEnd || '', maintenanceReason: truck.maintenanceReason || '' });
    else setForm(blank);
  }, [open, truck]);
  function submit() {
    if (!form.name.trim()) return;
    const payload = { ...form, assignedDriverId: form.assignedDriverId || null, maintenanceStart: form.maintenanceStart || null, maintenanceEnd: form.maintenanceEnd || null };
    if (isEdit) ctx.updateTruck(truck.id, payload);
    else ctx.addTruck(payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={isEdit ? `Edit Truck — ${truck.name}` : 'Add Truck'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Truck'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Name / Label"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. Truck 1" /></Field>
          <Field label="Plate Number"><TextInput value={form.plateNumber} onChange={e => setForm({ ...form, plateNumber: e.target.value })} /></Field>
        </div>
        <Field label="Assigned Driver">
          <Select value={form.assignedDriverId} onChange={e => setForm({ ...form, assignedDriverId: e.target.value })}>
            <option value="">— none —</option>
            {ctx.teamDirectory.filter(p => p.securityRole === 'Delivery Driver' && p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <p className="text-xs text-[var(--leon-black)]/50 pt-1 border-t border-[var(--leon-line)]">Maintenance window — while set, deliveries can't be scheduled against this truck's assigned driver for these dates.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Maintenance Start"><TextInput type="date" value={form.maintenanceStart} onChange={e => setForm({ ...form, maintenanceStart: e.target.value })} /></Field>
          <Field label="Maintenance End"><TextInput type="date" value={form.maintenanceEnd} onChange={e => setForm({ ...form, maintenanceEnd: e.target.value })} /></Field>
        </div>
        <Field label="Reason"><TextInput value={form.maintenanceReason} onChange={e => setForm({ ...form, maintenanceReason: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function LogisticsDashboardHome({ ctx }) {
  const today = todayISO();
  const containers = containerShipmentRows(ctx.exportContainers, ctx.projects);
  const activeContainers = containers.filter(c => c.status !== 'Delivered');
  const inTransit = containers.filter(c => c.status === 'In Transit');
  const arriving14 = containers.filter(c => c.eta && !c.actualArrival && daysBetween(today, c.eta) >= 0 && daysBetween(today, c.eta) <= 14);
  const delayed = containers.filter(c => containerRiskStatus(c) === 'Delayed');
  const atRisk = containers.filter(c => containerRiskStatus(c) === 'At Risk');
  const inCustoms = containers.filter(c => ['Documents Submitted', 'In Review', 'Held / Inspection'].includes(c.customsStatus));
  const awaitingPickup = containers.filter(c => c.status === 'Arrived' && !c.actualPickupDate);
  const upcomingDepartures = containers.filter(c => c.etd && !c.actualDeparture && daysBetween(today, c.etd) >= 0 && daysBetween(today, c.etd) <= 14).sort((a, b) => (a.etd < b.etd ? -1 : 1));
  const upcomingArrivals = [...arriving14].sort((a, b) => (a.eta < b.eta ? -1 : 1));
  const atRiskOrDelayed = containers.filter(c => ['At Risk', 'Delayed'].includes(containerRiskStatus(c)));
  const needsAction = containers.filter(c => (c.status === 'Arrived' && !c.actualPickupDate) || c.customsStatus === 'Held / Inspection' || (c.status !== 'Delivered' && EXPORT_DOC_CHECKLIST_STEPS.some(s => !docStepSatisfied(c.documents[s.key]))));

  const deliveries = ctx.projects.flatMap(p => p.deliveries.map(d => ({ ...d, projectId: p.id, projectName: p.name, scope: p.scopes.find(s => s.id === d.scopeId) })));
  const scheduledDeliveries = deliveries.filter(d => d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered');
  const deliveriesThisWeek = scheduledDeliveries.filter(d => d.date && daysBetween(today, d.date) >= 0 && daysBetween(today, d.date) <= 7).sort((a, b) => (a.date < b.date ? -1 : 1));
  const upcomingJobsiteDeliveries = [...scheduledDeliveries].filter(d => d.date && daysBetween(today, d.date) >= 0 && daysBetween(today, d.date) <= 14).sort((a, b) => (a.date < b.date ? -1 : 1));

  const openClaims = ctx.logisticsClaims.filter(c => !['Resolved', 'Denied'].includes(c.status));
  const inventoryValue = ctx.warehouseMaterials.reduce((s, m) => s + (m.currentStock || 0) * (m.unitCost || 0), 0);
  const unallocatedValue = ctx.warehouseMaterials.reduce((s, m) => s + availableQuantity(m, ctx.materialAllocations) * (m.unitCost || 0), 0);
  const monthStr = today.slice(0, 7);
  const thisMonthContainers = containers.filter(c => (c.actualDeparture || c.etd || '').startsWith(monthStr));
  const logisticsCostMtd = thisMonthContainers.reduce((s, c) => s + logisticsCostTotal(c.costs), 0);
  const demurrageMtd = thisMonthContainers.reduce((s, c) => s + (Number(c.costs && c.costs.demurrage) || 0) + (Number(c.costs && c.costs.detention) || 0), 0);
  // Reads the same ctx.tariffLines the Trade Compliance & Tariffs module and
  // the Export tab's "Tariffs & Customs" section use — not a duplicate figure.
  const openTariffExposure = ctx.tariffLines.filter(l => l.status !== 'Cleared').reduce((s, l) => s + (l.estimatedTariff || 0), 0);

  // Three BANDS, because the fifteen figures answer three different questions:
  // what is moving, what needs somebody, and what it is costing. Fifteen
  // identical grey boxes made the reader scan all of them to find the one that
  // mattered. `alert` fires only when the number is non-zero — a wall of red
  // zeroes would be worse than the grey wall it replaced, and a quiet board is
  // the correct picture of a quiet day.
  const kpis = [
    { g: 'moving', icon: '🚢', label: 'Active Shipments', value: activeContainers.length, onClick: () => ctx.goReport('logisticsMaster') },
    { g: 'moving', icon: '🧭', label: 'Containers In Transit', value: inTransit.length, onClick: () => ctx.goReport('logisticsMaster', { status: 'In Transit' }) },
    { g: 'moving', icon: '📅', label: 'Arriving Within 14 Days', value: arriving14.length, onClick: () => ctx.goReport('logisticsForecast') },
    { g: 'moving', icon: '🛃', label: 'Shipments In Customs', value: inCustoms.length, onClick: () => ctx.goReport('logisticsMaster') },
    { g: 'moving', icon: '🚚', label: 'Deliveries Scheduled This Week', value: deliveriesThisWeek.length, onClick: () => ctx.goReport('deliverySchedule') },
    { g: 'moving', icon: '📋', label: 'Open Deliveries', value: scheduledDeliveries.length, onClick: () => ctx.goReport('deliverySchedule') },

    { g: 'attention', icon: '⏰', label: 'Delayed Shipments', value: delayed.length, alert: delayed.length, onClick: () => ctx.goReport('logisticsDelayed', { risk: 'Delayed' }) },
    { g: 'attention', icon: '⚠️', label: 'At-Risk Shipments', value: atRisk.length, alert: atRisk.length, onClick: () => ctx.goReport('logisticsDelayed', { risk: 'At Risk' }) },
    { g: 'attention', icon: '📥', label: 'Containers Awaiting Pickup', value: awaitingPickup.length, alert: awaitingPickup.length, onClick: () => ctx.goReport('logisticsTracking') },
    { g: 'attention', icon: '🛠️', label: 'Open Damage/Shortage Cases', value: openClaims.length, alert: openClaims.length, onClick: () => ctx.goReport('logisticsClaimsReport') },

    { g: 'money', icon: '📦', label: 'Warehouse Inventory Value', value: fmtMoney(inventoryValue), financial: true, onClick: () => ctx.goReport('warehouseInventory') },
    { g: 'money', icon: '🏷️', label: 'Unallocated Inventory Value', value: fmtMoney(unallocatedValue), financial: true, onClick: () => ctx.goReport('unallocatedInventory') },
    { g: 'money', icon: '💵', label: 'Logistics Cost (MTD)', value: fmtMoney(logisticsCostMtd), financial: true, onClick: () => ctx.goReport('freightCostReport') },
    { g: 'money', icon: '⏳', label: 'Demurrage/Detention (MTD)', value: fmtMoney(demurrageMtd), financial: true, alert: demurrageMtd, onClick: () => ctx.goReport('demurrageReport') },
    { g: 'money', icon: '🛃', label: 'Open Tariff Exposure', value: fmtMoney(openTariffExposure), financial: true, alert: openTariffExposure, onClick: () => ctx.goTradeCompliance('exposure') },
  ].filter(k => !k.financial || ctx.canSeeFin);

  function ContainerRow({ c }) {
    return (
      <div key={c.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0 cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => ctx.goProjectTab(c.projectId, 'export')}>
        <div className="min-w-0">
          <p className="text-sm font-semibold truncate">{c.containerNumber} <span className="font-normal text-[var(--leon-black)]/50">— {c.projectName}</span></p>
          <p className="text-xs text-[var(--leon-black)]/50">{c.fromPort || '—'} → {c.toPort || '—'} · ETD {fmtDate(c.etd)} · ETA {fmtDate(c.eta)}</p>
        </div>
        <RiskBadge status={containerRiskStatus(c)} />
      </div>
    );
  }

  return (
    <div>
      <div className="mb-5 flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h1 className="text-2xl font-bold">Logistics Dashboard</h1>
          <p className="text-sm text-[var(--leon-black)]/50">Procurement → Export → Shipment → Customs → Warehouse → Allocation → Delivery, across every project.</p>
        </div>
        {ctx.canSeeTradeCompliance && <Button variant="outline" onClick={() => ctx.goTradeCompliance()}>Trade Compliance &amp; Tariffs →</Button>}
      </div>

      <div className="space-y-5 mb-6">
        {LOGISTICS_KPI_BANDS.map(band => {
          const items = kpis.filter(k => k.g === band.key);
          if (!items.length) return null;
          const live = items.filter(k => k.alert).length;
          return (
            <div key={band.key}>
              <div className="flex items-baseline gap-2 mb-2">
                <span aria-hidden="true">{band.icon}</span>
                <h2 className="text-[11px] font-bold uppercase tracking-[0.18em] text-[var(--leon-black)]/45">{band.label}</h2>
                <span className="text-[11px] text-[var(--leon-black)]/35">{band.hint}</span>
                {band.key === 'attention' && (
                  <span className={`ml-auto text-[11px] font-semibold ${live ? 'text-[var(--leon-red)]' : 'text-emerald-700'}`}>
                    {live ? `${live} needing attention` : 'Nothing needs attention'}
                  </span>
                )}
              </div>
              <div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3">
                {items.map((k, i) => (
                  <button key={i} onClick={k.onClick}
                    className={`text-left rounded-lg border bg-white overflow-hidden hover:shadow-sm transition-all cursor-pointer
                      ${k.alert ? 'border-[var(--leon-red)]/35 hover:border-[var(--leon-red)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]'}`}>
                    <div className="flex items-stretch">
                      <span aria-hidden="true" className={`w-1 shrink-0 ${k.alert ? 'bg-[var(--leon-red)]' : band.stripe}`} />
                      <span className="flex items-center gap-2.5 p-3 min-w-0">
                        <span aria-hidden="true"
                          className={`shrink-0 w-8 h-8 rounded-full grid place-items-center text-[15px] ${k.alert ? 'bg-red-50' : band.tint}`}>
                          {k.icon}
                        </span>
                        <span className="min-w-0">
                          <span className="block text-[10px] uppercase tracking-wide text-[var(--leon-black)]/50 font-semibold truncate">{k.label}</span>
                          <span className={`block text-lg font-bold tabular-nums ${k.alert ? 'text-[var(--leon-red)]' : ''}`}>{k.value}</span>
                        </span>
                      </span>
                    </div>
                  </button>
                ))}
              </div>
            </div>
          );
        })}
      </div>

      <div className="grid lg:grid-cols-2 gap-4">
        <Collapsible title="Upcoming Departures" count={upcomingDepartures.length}>
          {upcomingDepartures.length === 0 ? <EmptyState text="No departures in the next 14 days." /> : upcomingDepartures.map(c => <ContainerRow key={c.id} c={c} />)}
        </Collapsible>
        <Collapsible title="Upcoming Arrivals" count={upcomingArrivals.length}>
          {upcomingArrivals.length === 0 ? <EmptyState text="No arrivals in the next 14 days." /> : upcomingArrivals.map(c => <ContainerRow key={c.id} c={c} />)}
        </Collapsible>
        <Collapsible title="At-Risk Shipments" count={atRiskOrDelayed.length}>
          {atRiskOrDelayed.length === 0 ? <EmptyState text="Nothing at risk right now." /> : atRiskOrDelayed.map(c => <ContainerRow key={c.id} c={c} />)}
        </Collapsible>
        <Collapsible title="Containers Requiring Action" count={needsAction.length}>
          {needsAction.length === 0 ? <EmptyState text="No containers need action." /> : needsAction.map(c => <ContainerRow key={c.id} c={c} />)}
        </Collapsible>
        <Collapsible title="Upcoming Jobsite Deliveries" count={upcomingJobsiteDeliveries.length}>
          {upcomingJobsiteDeliveries.length === 0 ? <EmptyState text="No deliveries scheduled in the next 14 days." /> : upcomingJobsiteDeliveries.map(d => (
            <div key={d.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0 cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => ctx.goProjectTab(d.projectId, 'delivery', 'scheduled')}>
              <div className="min-w-0">
                <p className="text-sm font-semibold truncate">{d.deliveryNumber} <span className="font-normal text-[var(--leon-black)]/50">— {d.projectName}</span></p>
                <p className="text-xs text-[var(--leon-black)]/50">{d.scope ? d.scope.name : '—'} · {fmtDate(d.date)}</p>
              </div>
              <StatusBadge status={d.deliveryStatus} />
            </div>
          ))}
        </Collapsible>
        <Collapsible title="Open Logistics Issues" count={openClaims.length}>
          {openClaims.length === 0 ? <EmptyState text="No open damage/shortage/claim cases." /> : openClaims.map(c => {
            const project = ctx.projects.find(p => p.id === c.projectId);
            return (
              <div key={c.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0">
                <div className="min-w-0">
                  <p className="text-sm font-semibold truncate">{c.claimNumber} — {c.claimType}{project ? ` — ${project.name}` : ''}</p>
                  <p className="text-xs text-[var(--leon-black)]/50 truncate">{c.description || '—'}</p>
                </div>
                <StatusBadge status={c.status} />
              </div>
            );
          })}
        </Collapsible>
      </div>
    </div>
  );
}
// Cross-project production log, filterable by project/vendor/scope. Dates
// shown here are NOT a second, independently-entered copy — every record's
// start/end is read live from that scope's "Production" stage in Scopes &
// Schedule (productionDateRange, lib.jsx), the same field the Scopes &
// Schedule tab's Start/Complete/Report Delay buttons already write to. One
// database, two views. A scope with no production stage yet (shouldn't
// normally happen — every scope gets one from STAGE_DEFS) shows "Not
// scheduled" rather than a fabricated date.
function ProductionTimelineView({ ctx }) {
  const [projectFilter, setProjectFilter] = useState('All');
  const [vendorFilter, setVendorFilter] = useState('All');
  const [scopeFilter, setScopeFilter] = useState('All');
  const [detailFor, setDetailFor] = useState(null);
  const [view, setView] = useState('timeline');

  const allRecords = ctx.projects.flatMap(p => (p.productionRecords || []).map(r => {
    const scope = p.scopes.find(s => s.id === r.scopeId);
    const range = productionDateRange(p, r.scopeId);
    return { ...r, projectId: p.id, projectName: p.name, scopeName: scope ? scope.name : '—', startDate: range.start, endDate: range.end, scheduleStage: range.stage };
  }));
  const vendorNames = [...new Set(allRecords.map(r => r.vendorName))].sort();
  const scopeOptions = projectFilter === 'All' ? [] : (ctx.projects.find(p => p.id === projectFilter) || { scopes: [] }).scopes;

  const filtered = allRecords.filter(r =>
    (projectFilter === 'All' || r.projectId === projectFilter) &&
    (vendorFilter === 'All' || r.vendorName === vendorFilter) &&
    (scopeFilter === 'All' || r.scopeId === scopeFilter)
  );

  const detailModal = (
    <RecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} title={detailFor ? `${detailFor.vendorName} — ${detailFor.projectName}` : ''} printable
      fields={detailFor ? [
        { label: 'Project', value: detailFor.projectName }, { label: 'Scope', value: detailFor.scopeName },
        { label: 'Status', value: detailFor.status },
        { label: 'Start Date', value: detailFor.startDate ? fmtDate(detailFor.startDate) : 'Not scheduled' },
        { label: 'End Date', value: detailFor.endDate ? fmtDate(detailFor.endDate) : 'Not scheduled' },
        { label: 'Dates From', value: 'Scopes & Schedule — "Production" stage (edit there to change)' },
        { label: 'Created By', value: detailFor.createdBy }, { label: 'Notes', value: detailFor.notes || '—' },
      ] : []}
    />
  );

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-3">
        <div>
          <h1 className="text-2xl font-bold">Production Timeline</h1>
          <p className="text-sm text-[var(--leon-black)]/50">Every production record across every project, filterable by project, vendor, or scope.</p>
        </div>
        <div className="flex gap-1">
          <button onClick={() => setView('list')} className={`px-3 py-1.5 text-sm font-semibold rounded-l-lg border ${view === 'list' ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>List</button>
          <button onClick={() => setView('timeline')} className={`px-3 py-1.5 text-sm font-semibold rounded-r-lg border -ml-px ${view === 'timeline' ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>Timeline</button>
        </div>
      </div>
      <ProductionTimelineFilters ctx={ctx} projectFilter={projectFilter} setProjectFilter={setProjectFilter} vendorFilter={vendorFilter} setVendorFilter={setVendorFilter} scopeFilter={scopeFilter} setScopeFilter={setScopeFilter} vendorNames={vendorNames} scopeOptions={scopeOptions} />
      {filtered.length === 0 ? <EmptyState text="No production records match." /> : view === 'list' ? (
        <ProductionRecordListView records={filtered} onOpen={setDetailFor} />
      ) : (
        <ProductionRecordGanttView records={filtered} onOpen={setDetailFor} />
      )}
      {detailModal}
    </div>
  );
}
function ProductionRecordListView({ records, onOpen }) {
  const sorted = [...records].sort((a, b) => ((a.startDate || '9999') < (b.startDate || '9999') ? -1 : 1));
  return (
    <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
      <table className="w-full text-xs">
        <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase">
          <th className="px-3 py-2">Project</th><th className="px-3 py-2">Scope</th><th className="px-3 py-2">Vendor</th>
          <th className="px-3 py-2">Start Date</th><th className="px-3 py-2">End Date</th><th className="px-3 py-2">Status</th>
        </tr></thead>
        <tbody>
          {sorted.map(r => (
            <tr key={r.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => onOpen(r)}>
              <td className="px-3 py-2">{r.projectName}</td>
              <td className="px-3 py-2">{r.scopeName}</td>
              <td className="px-3 py-2 font-semibold">{r.vendorName}</td>
              <td className="px-3 py-2">{r.startDate ? fmtDate(r.startDate) : <span className="text-[var(--leon-black)]/40 italic">Not scheduled</span>}</td>
              <td className="px-3 py-2">{r.endDate ? fmtDate(r.endDate) : <span className="text-[var(--leon-black)]/40 italic">Not scheduled</span>}</td>
              <td className="px-3 py-2"><StatusBadge status={r.status} /></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
// Maps production states onto the shared schedule palette (SCHEDULE_COLORS, lib.jsx).
const PRODUCTION_BAR_STATUS = { 'Not Started': 'Not Started', 'In Production': 'In Progress', 'Quality Check': 'On Track', 'Complete': 'Complete' };
function ProductionRecordGanttView({ records, onOpen }) {
  const scheduled = records.filter(r => r.startDate);
  const unscheduled = records.filter(r => !r.startDate);
  if (scheduled.length === 0) return <EmptyState text="None of these records' scopes have a scheduled Production stage yet." />;
  const starts = scheduled.map(r => r.startDate);
  const ends = scheduled.map(r => r.endDate || addDays(r.startDate, 7));
  const minDate = addDays(starts.reduce((a, b) => (a < b ? a : b)), -3);
  const maxDate = addDays(ends.reduce((a, b) => (a > b ? a : b)), 3);
  const totalDays = Math.max(1, daysBetween(minDate, maxDate));
  const pctFor = d => Math.max(0, Math.min(100, (daysBetween(minDate, d) / totalDays) * 100));
  const months = [];
  let cursor = fromISO(minDate); cursor.setDate(1);
  const end = fromISO(maxDate);
  while (cursor <= end) { months.push(toISO(cursor)); cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1); }
  const today = todayISO();
  const todayInRange = today >= minDate && today <= maxDate;
  const byVendor = {};
  scheduled.forEach(r => { (byVendor[r.vendorName] = byVendor[r.vendorName] || []).push(r); });
  const LABEL_W = 200;

  return (
    <div className="overflow-x-auto border border-[var(--leon-line)] rounded-xl bg-white">
      <div style={{ minWidth: '900px' }}>
        <div className="flex border-b border-[var(--leon-line)]">
          <div className="shrink-0 border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} />
          <div className="relative flex-1 h-8">
            {months.map(m => (
              <div key={m} className="absolute top-0 h-full flex items-center text-[11px] font-bold text-[var(--leon-black)]/60 uppercase border-l border-[var(--leon-line)] pl-1.5" style={{ left: `${pctFor(m)}%` }}>
                {fromISO(m).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })}
              </div>
            ))}
          </div>
        </div>
        <div className="relative">
          <div className="absolute inset-0 pointer-events-none" style={{ left: LABEL_W }}>
            {months.map(m => <div key={m} className="absolute top-0 bottom-0 border-l border-[var(--leon-line)]" style={{ left: `${pctFor(m)}%` }} />)}
            {todayInRange && <div className="absolute top-0 bottom-0 w-0.5 bg-[var(--leon-red)]" style={{ left: `${pctFor(today)}%` }} />}
          </div>
          {Object.entries(byVendor).map(([vendor, vendorRecords], vi) => (
            <div key={vendor}>
              <div className="flex items-center" style={{ background: vi % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                <div className="shrink-0 px-3 py-1 text-xs font-bold truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={vendor}>{vendor}</div>
                <div className="flex-1" style={{ height: 8 }} />
              </div>
              {vendorRecords.map(r => {
                const rangeEnd = r.endDate || addDays(r.startDate, 7);
                const leftPct = pctFor(r.startDate);
                const widthPct = Math.max(0.6, pctFor(rangeEnd) - leftPct);
                return (
                  <div key={r.id} className="flex items-center" style={{ background: vi % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                    <div className="shrink-0 pl-4 pr-3 py-1 text-[11px] text-[var(--leon-black)]/70 truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={`${r.projectName} — ${r.scopeName}`}>{r.projectName} — {r.scopeName}</div>
                    <div className="relative flex-1" style={{ height: 28 }}>
                      <button
                        onClick={() => onOpen(r)}
                        className="absolute rounded-md shadow-sm hover:opacity-80"
                        style={{ left: `${leftPct}%`, width: `${widthPct}%`, top: 5, height: 18, minWidth: 8, borderRadius: 999, background: scheduleGradient(PRODUCTION_BAR_STATUS[r.status]), boxShadow: '0 1px 3px rgba(22,19,17,.18)' }}
                        title={`${r.vendorName} — ${r.status} (${fmtDate(r.startDate)} – ${fmtDate(r.endDate)})`}
                      />
                    </div>
                  </div>
                );
              })}
            </div>
          ))}
        </div>
      </div>
      <div className="flex items-center gap-4 p-2 text-[11px] text-[var(--leon-black)]/60 flex-wrap border-t border-[var(--leon-line)]">
        {PRODUCTION_STATUSES.map(s => <span key={s} className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(PRODUCTION_BAR_STATUS[s]) }} /> {s}</span>)}
        <span className="flex items-center gap-1.5"><span className="w-0.5 h-3 bg-[var(--leon-red)] inline-block" /> Today</span>
      </div>
      {unscheduled.length > 0 && (
        <p className="text-[11px] text-[var(--leon-black)]/40 px-2 pb-2">{unscheduled.length} record{unscheduled.length === 1 ? '' : 's'} not shown — no Production stage on file for that scope.</p>
      )}
    </div>
  );
}
function ProductionTimelineFilters({ ctx, projectFilter, setProjectFilter, vendorFilter, setVendorFilter, scopeFilter, setScopeFilter, vendorNames, scopeOptions }) {
  return (
    <div className="flex items-center gap-2 flex-wrap mb-3">
      <Select value={projectFilter} onChange={e => { setProjectFilter(e.target.value); setScopeFilter('All'); }} className="!w-auto !py-1 !text-xs">
        <option value="All">All Projects</option>
        {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
      </Select>
      <Select value={vendorFilter} onChange={e => setVendorFilter(e.target.value)} className="!w-auto !py-1 !text-xs">
        <option value="All">All Vendors</option>
        {vendorNames.map(v => <option key={v} value={v}>{v}</option>)}
      </Select>
      <Select value={scopeFilter} onChange={e => setScopeFilter(e.target.value)} disabled={projectFilter === 'All'} className="!w-auto !py-1 !text-xs">
        <option value="All">All Scopes</option>
        {scopeOptions.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
      </Select>
    </div>
  );
}
// ============================================================================
// Employee Workload & Timeline — cross-project view of who's assigned to
// what and when, for the handful of management roles who need to see
// overload before it becomes a schedule problem (canSeeWorkload, data.jsx).
// Built from the same two live sources every other cross-project view here
// reads from: project.tasks (assigneeId/dueDate) and scope.stages
// (assignedUserId/plannedStart/plannedDue) — no separate workload record.
// ============================================================================
function buildWorkload(ctx) {
  const today = todayISO();
  const byPerson = {};
  function bucket(person) {
    if (!byPerson[person.id]) byPerson[person.id] = { person, tasks: [], stages: [], projectIds: new Set() };
    return byPerson[person.id];
  }
  ctx.projects.forEach(p => {
    p.tasks.forEach(t => {
      if (t.status === 'Completed') return;
      const person = ctx.teamDirectory.find(x => x.id === t.assigneeId);
      if (!person) return;
      const b = bucket(person);
      b.tasks.push({ ...t, projectId: p.id, projectName: p.name });
      b.projectIds.add(p.id);
    });
    p.scopes.forEach(s => s.stages.forEach(st => {
      if (st.status === 'Completed' || !st.assignedUserId) return;
      const person = ctx.teamDirectory.find(x => x.id === st.assignedUserId);
      if (!person) return;
      const b = bucket(person);
      b.stages.push({ ...st, projectId: p.id, projectName: p.name, scopeName: s.name });
      b.projectIds.add(p.id);
    }));
    (p.chronology || []).forEach(st => {
      if (st.status === 'Completed' || !st.assignedUserId) return;
      const person = ctx.teamDirectory.find(x => x.id === st.assignedUserId);
      if (!person) return;
      const b = bucket(person);
      b.stages.push({ ...st, projectId: p.id, projectName: p.name, scopeName: 'Lead, Take-Off & Quotes' });
      b.projectIds.add(p.id);
    });
  });
  return Object.values(byPerson).map(b => {
    const openCount = b.tasks.length + b.stages.length;
    const deadlines = [...b.tasks.map(t => t.dueDate), ...b.stages.map(st => st.plannedDue)].filter(Boolean).sort();
    const overdue = [...b.tasks.filter(t => t.dueDate && t.dueDate < today), ...b.stages.filter(st => st.plannedDue && st.plannedDue < today)].length;
    return {
      person: b.person, tasks: b.tasks, stages: b.stages,
      activeProjects: b.projectIds.size, openCount, overdue,
      nextDeadline: deadlines[0] || null,
      workload: openCount === 0 ? 'None' : openCount <= 2 ? 'Light' : openCount <= 5 ? 'Moderate' : 'Heavy',
    };
  }).filter(w => w.openCount > 0).sort((a, b) => b.openCount - a.openCount);
}
const WORKLOAD_TONE = { None: 'neutral', Light: 'green', Moderate: 'yellow', Heavy: 'red' };
function WorkloadView({ ctx }) {
  const [view, setView] = useState('list');
  const [search, setSearch] = useState('');
  const [employeeFilter, setEmployeeFilter] = useState('All');
  const [detailFor, setDetailFor] = useState(null);
  const all = useMemo(() => buildWorkload(ctx), [ctx.projects, ctx.teamDirectory]);
  const filtered = all
    .filter(w => !search.trim() || w.person.name.toLowerCase().includes(search.trim().toLowerCase()))
    .filter(w => employeeFilter === 'All' || w.person.id === employeeFilter);

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-3">
        <div>
          <h1 className="text-2xl font-bold">Employee Workload & Timeline</h1>
          <p className="text-sm text-[var(--leon-black)]/50">Open tasks and assigned schedule stages, across every project. Only people with open work are shown.</p>
        </div>
        <div className="flex gap-1">
          <button onClick={() => setView('list')} className={`px-3 py-1.5 text-sm font-semibold rounded-l-lg border ${view === 'list' ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>List</button>
          <button onClick={() => setView('timeline')} className={`px-3 py-1.5 text-sm font-semibold rounded-r-lg border -ml-px ${view === 'timeline' ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>Timeline</button>
        </div>
      </div>
      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput placeholder="Search by name…" value={search} onChange={e => setSearch(e.target.value)} className="!w-56 !py-1.5 !text-sm" />
        <Select value={employeeFilter} onChange={e => setEmployeeFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Employees</option>
          {all.map(w => <option key={w.person.id} value={w.person.id}>{w.person.name}</option>)}
        </Select>
      </div>
      {filtered.length === 0 ? <EmptyState text="No matching workload." /> : view === 'list' ? (
        <WorkloadListView rows={filtered} onOpen={setDetailFor} />
      ) : (
        <WorkloadGanttView rows={filtered} onOpen={setDetailFor} />
      )}
      <WorkloadDetailModal open={!!detailFor} row={detailFor} onClose={() => setDetailFor(null)} ctx={ctx} />
    </div>
  );
}
function WorkloadListView({ rows, onOpen }) {
  return (
    <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
      <table className="w-full text-xs">
        <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase">
          <th className="px-3 py-2">Name</th><th className="px-3 py-2">Role</th><th className="px-3 py-2">Active Projects</th>
          <th className="px-3 py-2">Open Tasks</th><th className="px-3 py-2">Assigned Stages</th><th className="px-3 py-2">Overdue</th>
          <th className="px-3 py-2">Next Deadline</th><th className="px-3 py-2">Workload</th>
        </tr></thead>
        <tbody>
          {rows.map(w => (
            <tr key={w.person.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => onOpen(w)}>
              <td className="px-3 py-2 font-semibold">{w.person.name}</td>
              <td className="px-3 py-2 text-[var(--leon-black)]/60">{w.person.securityRole}</td>
              <td className="px-3 py-2">{w.activeProjects}</td>
              <td className="px-3 py-2">{w.tasks.length}</td>
              <td className="px-3 py-2">{w.stages.length}</td>
              <td className={`px-3 py-2 ${w.overdue > 0 ? 'text-[var(--leon-red)] font-semibold' : ''}`}>{w.overdue}</td>
              <td className="px-3 py-2">{fmtDate(w.nextDeadline)}</td>
              <td className="px-3 py-2"><Badge tone={WORKLOAD_TONE[w.workload]}>{w.workload}</Badge></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
function WorkloadDetailModal({ open, row, onClose, ctx }) {
  if (!row) return null;
  const items = [
    ...row.tasks.map(t => ({ id: t.id, label: t.title, projectName: t.projectName, date: t.dueDate, kind: 'Task', onOpen: () => ctx.goProjectTab(t.projectId, 'tasks') })),
    ...row.stages.map(st => ({ id: st.id, label: st.name, projectName: st.projectName, scopeName: st.scopeName, date: st.plannedDue, kind: 'Stage', onOpen: () => ctx.goProjectTab(st.projectId, st.scopeName === 'Lead, Take-Off & Quotes' ? 'overview' : 'scopes') })),
  ].sort((a, b) => (a.date < b.date ? -1 : 1));
  return (
    <Modal open={open} onClose={onClose} wide title={`${row.person.name} — Workload`} footer={<Button variant="ghost" onClick={onClose}>Close</Button>}>
      <div className="space-y-1.5">
        {items.map(it => (
          <div key={it.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0 cursor-pointer hover:bg-[var(--leon-cream)]" onClick={it.onOpen}>
            <div className="min-w-0">
              <p className="text-sm font-semibold truncate">{it.label}</p>
              <p className="text-xs text-[var(--leon-black)]/50">{it.projectName}{it.scopeName ? ` — ${it.scopeName}` : ''} · {it.kind}</p>
            </div>
            <span className="text-xs text-[var(--leon-black)]/50 shrink-0">{fmtDate(it.date)}</span>
          </div>
        ))}
      </div>
    </Modal>
  );
}
function WorkloadGanttView({ rows, onOpen }) {
  const allDates = rows.flatMap(w => [
    ...w.stages.map(st => st.plannedStart), ...w.stages.map(st => st.plannedDue),
    ...w.tasks.map(t => t.dueDate),
  ]).filter(Boolean);
  if (allDates.length === 0) return <EmptyState text="Nothing to plot." />;
  const minDate = addDays(allDates.reduce((a, b) => (a < b ? a : b)), -3);
  const maxDate = addDays(allDates.reduce((a, b) => (a > b ? a : b)), 3);
  const totalDays = Math.max(1, daysBetween(minDate, maxDate));
  const pctFor = d => Math.max(0, Math.min(100, (daysBetween(minDate, d) / totalDays) * 100));
  const months = [];
  let cursor = fromISO(minDate); cursor.setDate(1);
  const end = fromISO(maxDate);
  while (cursor <= end) { months.push(toISO(cursor)); cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1); }
  const today = todayISO();
  const todayInRange = today >= minDate && today <= maxDate;
  const LABEL_W = 180;
  // Resolve a row to one of the shared schedule statuses (SCHEDULE_COLORS,
  // lib.jsx) so every Gantt in the app speaks the same colour language.
  function statusKey(status, dueDate) {
    if (status === 'Completed') return 'Complete';
    if (dueDate && dueDate < today) return 'Delayed';
    if (status === 'In Progress') return 'In Progress';
    return 'Not Started';
  }

  return (
    <div className="overflow-x-auto border border-[var(--leon-line)] rounded-xl bg-white">
      <div style={{ minWidth: '900px' }}>
        <div className="flex border-b border-[var(--leon-line)]">
          <div className="shrink-0 border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} />
          <div className="relative flex-1 h-8">
            {months.map(m => (
              <div key={m} className="absolute top-0 h-full flex items-center text-[11px] font-bold text-[var(--leon-black)]/60 uppercase border-l border-[var(--leon-line)] pl-1.5" style={{ left: `${pctFor(m)}%` }}>
                {fromISO(m).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })}
              </div>
            ))}
          </div>
        </div>
        <div className="relative">
          <div className="absolute inset-0 pointer-events-none" style={{ left: LABEL_W }}>
            {months.map(m => <div key={m} className="absolute top-0 bottom-0 border-l border-[var(--leon-line)]" style={{ left: `${pctFor(m)}%` }} />)}
            {todayInRange && <div className="absolute top-0 bottom-0 w-0.5 bg-[var(--leon-red)]" style={{ left: `${pctFor(today)}%` }} />}
          </div>
          {rows.map((w, ri) => {
            const items = [
              ...w.stages.map(st => ({ id: st.id, label: `${st.projectName} — ${st.name}`, start: st.plannedStart, end: st.plannedDue, status: st.status, dueDate: st.plannedDue })),
              ...w.tasks.map(t => ({ id: t.id, label: `${t.projectName} — ${t.title}`, start: t.dueDate, end: t.dueDate, status: t.status, dueDate: t.dueDate })),
            ];
            return (
              <div key={w.person.id}>
                <div className="flex items-center" style={{ background: ri % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                  <div className="shrink-0 px-3 py-1 text-xs font-bold truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={w.person.name}>{w.person.name}</div>
                  <div className="flex-1" style={{ height: 6 }} />
                </div>
                {items.map(it => {
                  const leftPct = pctFor(it.start);
                  const widthPct = Math.max(0.6, pctFor(it.end) - leftPct);
                  return (
                    <div key={it.id} className="flex items-center" style={{ background: ri % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                      <div className="shrink-0 pl-4 pr-3 py-1 text-[11px] text-[var(--leon-black)]/70 truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={it.label}>{it.label}</div>
                      <div className="relative flex-1" style={{ height: 26 }}>
                        <div
                          className="absolute rounded-full transition-transform hover:scale-y-125 hover:z-10"
                          style={{ left: `${leftPct}%`, width: `${widthPct}%`, top: 5, height: 16, minWidth: 8,
                                   background: scheduleGradient(statusKey(it.status, it.dueDate)),
                                   boxShadow: '0 1px 3px rgba(22,19,17,.18)' }}
                          title={`${it.label}: ${fmtDate(it.start)} – ${fmtDate(it.end)} (${it.status})`}
                        />
                      </div>
                    </div>
                  );
                })}
              </div>
            );
          })}
        </div>
      </div>
      <div className="flex items-center gap-4 p-2 text-[11px] text-[var(--leon-black)]/60 flex-wrap border-t border-[var(--leon-line)]">
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient('Not Started') }} /> Not Started / Open</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient('In Progress') }} /> In Progress</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient('Complete') }} /> Completed</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient('Delayed') }} /> Overdue</span>
        <span className="flex items-center gap-1.5"><span className="w-0.5 h-3 bg-[var(--leon-red)] inline-block" /> Today</span>
      </div>
    </div>
  );
}
// ============================================================================
// Trade Compliance & Tariffs — centralized tariff/duty/customs database.
// Deliberately reads/writes the SAME tariffLibrary/tariffLines collections
// the Export tab's "Tariffs & Customs" section and the Logistics reports
// use — this file has no parallel tariff data store. See data.jsx for the
// versioned-rate model (a rate change never overwrites; it appends a new
// version with its own effective dates) and the estimated/actual split on
// each line (never overwritten into one another, for estimating-accuracy
// analysis).
// ============================================================================
function enrichedTariffLines(ctx) {
  return ctx.tariffLines.map(l => {
    const project = ctx.projects.find(p => p.id === l.projectId);
    const scope = project ? project.scopes.find(s => s.id === l.scopeId) : null;
    const vendor = ctx.vendors.find(v => v.id === l.vendorId);
    const container = ctx.exportContainers.find(c => c.id === l.exportContainerId) || null;
    const classification = ctx.tariffLibrary.find(c => c.id === l.tariffClassificationId);
    const broker = l.actual && l.actual.brokerId ? ctx.freightForwarders.find(f => f.id === l.actual.brokerId) : null;
    return {
      ...l, projectName: project ? project.name : '—', scopeName: scope ? scope.name : '—',
      vendorName: vendor ? vendor.name : '—', containerNumber: container ? container.containerNumber : '—',
      classification, brokerName: broker ? broker.name : '—',
    };
  });
}
const TRADE_COMPLIANCE_SUBTABS = [
  { key: 'dashboard', label: 'Dashboard', icon: '📊' },
  { key: 'library', label: 'Tariff Library', icon: '🛃' },
  { key: 'lines', label: 'Tariff Lines', icon: '🧾' },
  { key: 'exposure', label: 'Tariff Exposure', icon: '📈' },
];
function TradeComplianceView({ ctx, pendingSubtab, onConsumeSubtab }) {
  const [sub, setSub] = useState(pendingSubtab || 'dashboard');
  useEffect(() => { if (pendingSubtab) { setSub(pendingSubtab); onConsumeSubtab && onConsumeSubtab(); } }, [pendingSubtab]);
  return (
    <div>
      <div className="mb-5">
        <h1 className="text-2xl font-bold">Trade Compliance & Tariffs</h1>
        <p className="text-sm text-[var(--leon-black)]/50">Tariff/duty/customs classifications and cost tracking — the same underlying records the Export and Logistics tabs read and write.</p>
      </div>
      <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)] flex-wrap">
        {TRADE_COMPLIANCE_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}
            {t.label}
          </button>
        ))}
      </div>
      {sub === 'dashboard' && <TradeComplianceDashboard ctx={ctx} onNavigate={setSub} />}
      {sub === 'library' && <TariffLibrarySubTab ctx={ctx} />}
      {sub === 'lines' && <TariffLinesSubTab ctx={ctx} />}
      {sub === 'exposure' && <TariffExposureSubTab ctx={ctx} onNavigate={setSub} />}
    </div>
  );
}
function TradeComplianceDashboard({ ctx, onNavigate }) {
  const lines = useMemo(() => enrichedTariffLines(ctx), [ctx.tariffLines, ctx.projects, ctx.vendors, ctx.tariffLibrary]);
  const cleared = lines.filter(l => l.status === 'Cleared');
  const open = lines.filter(l => l.status !== 'Cleared');
  const totalActualTariffs = cleared.reduce((s, l) => s + (l.actualTariff || 0), 0);
  const totalEstimatedTariffs = lines.reduce((s, l) => s + (l.estimatedTariff || 0), 0);
  const totalVariance = cleared.reduce((s, l) => s + (l.varianceAmount || 0), 0);
  const totalExposure = open.reduce((s, l) => s + (l.estimatedTariff || 0), 0);
  const totalOpenValue = open.reduce((s, l) => s + (l.customsValue || 0), 0);
  const totalMaterialCost = ctx.projects.flatMap(p => p.scopes).reduce((s, sc) => s + (sc.profitability ? (sc.profitability.actual.vendorCost || sc.profitability.costs.vendorCost) : 0), 0);
  const totalContractValue = ctx.projects.reduce((s, p) => s + revisedContractValue(p), 0);

  function topN(keyFn, labelFn, valueLines, n) {
    const buckets = {};
    valueLines.forEach(l => {
      const key = keyFn(l);
      if (!key || key === '—') return;
      buckets[key] = (buckets[key] || 0) + (l.actualTariff !== null && l.actualTariff !== undefined ? l.actualTariff : l.estimatedTariff);
    });
    return Object.entries(buckets).sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => ({ label: labelFn ? labelFn(k) : k, value: v }));
  }
  const byProject = topN(l => l.projectName, null, lines, 5);
  const byVendor = topN(l => l.vendorName, null, lines, 5);
  const byCountry = topN(l => l.countryOfOrigin, null, lines, 5);
  const byHts = topN(l => l.htsCode || l.hsCode, null, lines, 5);

  const recentChanges = ctx.tariffLibrary
    .flatMap(c => c.changeLog.map(e => ({ ...e, classification: c })))
    .filter(e => daysBetween(e.date.slice(0, 10), todayISO()) <= 30)
    .sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 8);
  const upcoming = ctx.tariffLibrary.flatMap(c => c.versions.filter(v => v.effectiveFrom > todayISO()).map(v => ({ classification: c, version: v })))
    .sort((a, b) => (a.version.effectiveFrom < b.version.effectiveFrom ? -1 : 1));

  const kpis = [
    { label: 'Total Tariffs Paid (Actual)', value: fmtMoney(totalActualTariffs), onClick: () => ctx.goReport('tariffLinesMaster', { status: 'Cleared' }) },
    { label: 'Estimated Tariffs (All Lines)', value: fmtMoney(totalEstimatedTariffs), onClick: () => onNavigate('lines') },
    { label: 'Actual Tariffs', value: fmtMoney(totalActualTariffs), onClick: () => ctx.goReport('tariffLinesMaster', { status: 'Cleared' }) },
    { label: 'Est. vs Actual Variance', value: fmtMoney(totalVariance), tone: totalVariance > 0 ? 'red' : null, onClick: () => ctx.goReport('tariffVarianceReport') },
    { label: 'Current Tariff Exposure', value: fmtMoney(totalExposure), tone: 'red', onClick: () => onNavigate('exposure') },
    { label: 'Open Procurement Value', value: fmtMoney(totalOpenValue), onClick: () => onNavigate('exposure') },
    { label: 'Tariff Cost as % of Material Cost', value: totalMaterialCost ? fmtPct((totalActualTariffs / totalMaterialCost) * 100) : '—' },
    { label: 'Tariff Cost as % of Contract Value', value: totalContractValue ? fmtPct((totalActualTariffs / totalContractValue) * 100) : '—' },
    { label: 'Classifications Tracked', value: String(ctx.tariffLibrary.length), onClick: () => onNavigate('library') },
    { label: 'Tariff Lines Tracked', value: String(lines.length), onClick: () => onNavigate('lines') },
  ];

  return (
    <div>
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-3 mb-6">
        {kpis.map((k, i) => (
          <button key={i} onClick={k.onClick} disabled={!k.onClick} className={`text-left border border-[var(--leon-line)] rounded-lg p-3 bg-white ${k.onClick ? 'hover:border-[var(--leon-brown)] hover:bg-[var(--leon-cream)] cursor-pointer' : 'cursor-default'} transition-colors`}>
            <p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">{k.label}</p>
            <p className={`text-lg font-bold ${k.tone === 'red' ? 'text-[var(--leon-red)]' : ''}`}>{k.value}</p>
          </button>
        ))}
      </div>
      <div className="grid lg:grid-cols-2 gap-4">
        <TariffBreakdownCard title="Tariffs by Project" rows={byProject} onOpen={() => onNavigate('lines')} />
        <TariffBreakdownCard title="Tariffs by Vendor" rows={byVendor} onOpen={() => onNavigate('lines')} />
        <TariffBreakdownCard title="Tariffs by Country of Origin" rows={byCountry} onOpen={() => onNavigate('lines')} />
        <TariffBreakdownCard title="Tariffs by HTS Classification" rows={byHts} onOpen={() => onNavigate('lines')} />
        <Collapsible title="Recently Changed Tariff Classifications" count={recentChanges.length}>
          {recentChanges.length === 0 ? <EmptyState text="No classification changes in the last 30 days." /> : recentChanges.map(e => (
            <div key={e.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0">
              <div className="min-w-0">
                <p className="text-sm font-semibold truncate">{e.classification.htsCode || e.classification.hsCode} — {e.classification.productDescription || e.classification.materialCategory}</p>
                <p className="text-xs text-[var(--leon-black)]/50">{e.field}: {e.previousValue} → {e.newValue} · {e.user} · {fmtDate(e.date.slice(0, 10))}</p>
              </div>
            </div>
          ))}
        </Collapsible>
        <Collapsible title="Upcoming Tariff Changes" count={upcoming.length}>
          {upcoming.length === 0 ? <EmptyState text="No scheduled future rate changes." /> : upcoming.map(({ classification, version }) => (
            <div key={version.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0">
              <div className="min-w-0">
                <p className="text-sm font-semibold truncate">{classification.htsCode || classification.hsCode} — {classification.productDescription || classification.materialCategory}</p>
                <p className="text-xs text-[var(--leon-black)]/50">New rate {tariffVersionTotalPct(version)}% effective {fmtDate(version.effectiveFrom)}</p>
              </div>
              <Badge tone="yellow">{version.status}</Badge>
            </div>
          ))}
        </Collapsible>
      </div>
    </div>
  );
}
function TariffBreakdownCard({ title, rows, onOpen }) {
  return (
    <Collapsible title={title} count={rows.length}>
      {rows.length === 0 ? <EmptyState text="No data yet." /> : rows.map(r => (
        <button key={r.label} onClick={onOpen} className="w-full flex items-center justify-between gap-3 py-1.5 border-b border-[var(--leon-line)] last:border-0 text-left hover:bg-[var(--leon-cream)]">
          <span className="text-sm truncate">{r.label}</span>
          <span className="text-sm font-semibold shrink-0">{fmtMoney(r.value)}</span>
        </button>
      ))}
    </Collapsible>
  );
}
function TariffLibrarySubTab({ ctx }) {
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('All');
  const [originFilter, setOriginFilter] = useState('All');
  const [destFilter, setDestFilter] = useState('All');
  const [categoryFilter, setCategoryFilter] = useState('All');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const editable = ctx.canEditTariffClassification;
  // The pickers are built from what is actually IN the library, not a fixed
  // list, so importing a new country or product category makes it selectable
  // without a code change — and a filter can never offer an empty result.
  const origins = useMemo(
    () => Array.from(new Set(ctx.tariffLibrary.map(c => c.countryOfOrigin).filter(Boolean))).sort(),
    [ctx.tariffLibrary]);
  const destinations = useMemo(
    () => Array.from(new Set(ctx.tariffLibrary.map(c => c.destinationCountry).filter(Boolean))).sort(),
    [ctx.tariffLibrary]);
  const categories = useMemo(
    () => Array.from(new Set(ctx.tariffLibrary.map(c => c.materialCategory).filter(Boolean))).sort(),
    [ctx.tariffLibrary]);
  const filtered = ctx.tariffLibrary.filter(c => {
    const current = currentTariffVersion(c);
    if (statusFilter !== 'All' && (!current || current.status !== statusFilter)) return false;
    if (originFilter !== 'All' && c.countryOfOrigin !== originFilter) return false;
    if (destFilter !== 'All' && c.destinationCountry !== destFilter) return false;
    if (categoryFilter !== 'All' && c.materialCategory !== categoryFilter) return false;
    if (!search.trim()) return true;
    const q = search.trim().toLowerCase();
    return [c.hsCode, c.htsCode, c.productDescription, c.materialCategory, c.scopeFamily, c.countryOfOrigin].some(v => (v || '').toLowerCase().includes(q));
  });
  const anyFilter = statusFilter !== 'All' || originFilter !== 'All' || destFilter !== 'All'
    || categoryFilter !== 'All' || !!search.trim();
  return (
    <div>
      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput placeholder="Search HTS, HS, description, category…" value={search} onChange={e => setSearch(e.target.value)} className="!w-64 !py-1.5 !text-sm" />
        {/* Origin → destination as a pair, with the arrow between them, because
            that is the question being asked: what does THIS route cost. Reading
            them as two unrelated dropdowns is how you pick an impossible one. */}
        <span className="inline-flex items-center gap-1.5 rounded-md border border-[var(--leon-line)] bg-white px-2 py-1">
          <span className="text-[10px] uppercase tracking-wide font-semibold text-[var(--leon-black)]/40">From</span>
          <Select value={originFilter} onChange={e => setOriginFilter(e.target.value)} className="!w-auto !border-0 !py-0 !px-1 !text-sm !bg-transparent">
            <option value="All">Any origin</option>
            {origins.map(o => <option key={o} value={o}>{o}</option>)}
          </Select>
          <span aria-hidden="true" className="text-[var(--leon-black)]/35">&rarr;</span>
          <span className="text-[10px] uppercase tracking-wide font-semibold text-[var(--leon-black)]/40">To</span>
          <Select value={destFilter} onChange={e => setDestFilter(e.target.value)} className="!w-auto !border-0 !py-0 !px-1 !text-sm !bg-transparent">
            <option value="All">Any destination</option>
            {destinations.map(d => <option key={d} value={d}>{d}</option>)}
          </Select>
        </span>
        <Select value={categoryFilter} onChange={e => setCategoryFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Categories</option>
          {categories.map(c => <option key={c} value={c}>{c}</option>)}
        </Select>
        <Select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Statuses</option>
          {TARIFF_STATUSES.map(s => <option key={s}>{s}</option>)}
        </Select>
        {/* Say how much of the library you are looking at — 658 rows filtered to
            9 with no count is how someone concludes a code is missing. */}
        <span className="text-xs text-[var(--leon-black)]/45 whitespace-nowrap">
          {anyFilter ? `${filtered.length} of ${ctx.tariffLibrary.length}` : `${ctx.tariffLibrary.length} classifications`}
        </span>
        {anyFilter && (
          <Button size="sm" variant="ghost" onClick={() => { setSearch(''); setStatusFilter('All'); setOriginFilter('All'); setDestFilter('All'); setCategoryFilter('All'); }}>Clear</Button>
        )}
        {editable && <Button size="sm" className="ml-auto" onClick={() => setShowAdd(true)}>+ Add Classification</Button>}
      </div>
      {filtered.length === 0 ? <EmptyState text="No tariff classifications match." /> : (
        <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase">
              <th className="px-3 py-2">HTS / HS Code</th><th className="px-3 py-2">Product Description</th><th className="px-3 py-2">Category</th>
              <th className="px-3 py-2">Origin → Destination</th><th className="px-3 py-2">Total Duty %</th><th className="px-3 py-2">Status</th>
              <th className="px-3 py-2">Effective From</th><th className="px-3 py-2">Revisions</th>
            </tr></thead>
            <tbody>
              {filtered.map(c => {
                const v = currentTariffVersion(c);
                return (
                  <tr key={c.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => setDetailFor(c)}>
                    <td className="px-3 py-2 font-semibold">{c.htsCode || c.hsCode || '—'}</td>
                    <td className="px-3 py-2">{c.productDescription || '—'}</td>
                    <td className="px-3 py-2 text-[var(--leon-black)]/60">{c.materialCategory || '—'}</td>
                    <td className="px-3 py-2">{c.countryOfOrigin || '—'} → {c.destinationCountry || '—'}</td>
                    <td className="px-3 py-2 font-semibold">{v ? fmtPct(v.totalEstimatedDutyPct) : '—'}</td>
                    <td className="px-3 py-2">{v && <StatusBadge status={v.status} />}</td>
                    <td className="px-3 py-2">{v ? fmtDate(v.effectiveFrom) : '—'}</td>
                    <td className="px-3 py-2">{c.versions.length}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
      <AddTariffClassificationModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
      <TariffClassificationDetailModal open={!!detailFor} classification={detailFor ? ctx.tariffLibrary.find(c => c.id === detailFor.id) : null} onClose={() => setDetailFor(null)} ctx={ctx} />
    </div>
  );
}
function AddTariffClassificationModal({ open, onClose, ctx }) {
  const blank = {
    scopeFamily: '', materialCategory: MATERIAL_CATEGORIES[0], productDescription: '', hsCode: '', htsCode: '',
    countryOfOrigin: '', destinationCountry: 'USA', baseDutyPct: '', additionalTariffPct: '', section301Pct: '', antidumpingCvdPct: '',
    effectiveFrom: todayISO(), effectiveUntil: '', status: 'Active', source: '', notes: '',
  };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.productDescription.trim() || (!form.hsCode && !form.htsCode)) return;
    ctx.addTariffClassification({
      ...form,
      baseDutyPct: Number(form.baseDutyPct) || 0, additionalTariffPct: Number(form.additionalTariffPct) || 0,
      section301Pct: Number(form.section301Pct) || 0, antidumpingCvdPct: Number(form.antidumpingCvdPct) || 0,
      effectiveUntil: form.effectiveUntil || null,
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Add Tariff Classification" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Classification</Button></>}>
      <div className="space-y-3">
        <Field label="Product Description"><TextInput value={form.productDescription} onChange={e => setForm({ ...form, productDescription: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Material / Product Category"><Select value={form.materialCategory} onChange={e => setForm({ ...form, materialCategory: e.target.value })}>{MATERIAL_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
          <Field label="HS Code"><TextInput value={form.hsCode} onChange={e => setForm({ ...form, hsCode: e.target.value })} /></Field>
          <Field label="HTS Code"><TextInput value={form.htsCode} onChange={e => setForm({ ...form, htsCode: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Country of Origin"><TextInput value={form.countryOfOrigin} onChange={e => setForm({ ...form, countryOfOrigin: e.target.value })} /></Field>
          <Field label="Destination Country"><TextInput value={form.destinationCountry} onChange={e => setForm({ ...form, destinationCountry: e.target.value })} /></Field>
        </div>
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 pt-1">Initial Rate</p>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Base Duty %"><TextInput type="number" value={form.baseDutyPct} onChange={e => setForm({ ...form, baseDutyPct: e.target.value })} /></Field>
          <Field label="Additional Tariff %"><TextInput type="number" value={form.additionalTariffPct} onChange={e => setForm({ ...form, additionalTariffPct: e.target.value })} /></Field>
          <Field label="Section 301 / Other %"><TextInput type="number" value={form.section301Pct} onChange={e => setForm({ ...form, section301Pct: e.target.value })} /></Field>
          <Field label="Antidumping / CVD %"><TextInput type="number" value={form.antidumpingCvdPct} onChange={e => setForm({ ...form, antidumpingCvdPct: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Effective From"><TextInput type="date" value={form.effectiveFrom} onChange={e => setForm({ ...form, effectiveFrom: e.target.value })} /></Field>
          <Field label="Effective Until (optional)"><TextInput type="date" value={form.effectiveUntil} onChange={e => setForm({ ...form, effectiveUntil: e.target.value })} /></Field>
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{TARIFF_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
        </div>
        <Field label="Source / Reference" hint="e.g. USITC HTS Revision 12, CBP CSMS #..."><TextInput value={form.source} onChange={e => setForm({ ...form, source: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
// Edits only the non-versioned identity fields (description, codes, origin,
// category, notes) — the rate itself is never editable in place; a rate
// change always goes through AddTariffVersionModal so history is preserved.
function EditTariffClassificationModal({ open, onClose, ctx, classification }) {
  const blank = { scopeFamily: '', materialCategory: MATERIAL_CATEGORIES[0], productDescription: '', hsCode: '', htsCode: '', countryOfOrigin: '', destinationCountry: 'USA', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (open && classification) {
      setForm({
        scopeFamily: classification.scopeFamily, materialCategory: classification.materialCategory, productDescription: classification.productDescription,
        hsCode: classification.hsCode, htsCode: classification.htsCode, countryOfOrigin: classification.countryOfOrigin,
        destinationCountry: classification.destinationCountry, notes: classification.notes,
      });
    }
  }, [open, classification]);
  if (!classification) return null;
  function submit() {
    ctx.updateTariffClassification(classification.id, form, 'Edited classification details');
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Edit Classification — ${classification.htsCode || classification.hsCode}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Changes</Button></>}>
      <div className="space-y-3">
        <Field label="Product Description"><TextInput value={form.productDescription} onChange={e => setForm({ ...form, productDescription: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Material / Product Category"><Select value={form.materialCategory} onChange={e => setForm({ ...form, materialCategory: e.target.value })}>{MATERIAL_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
          <Field label="HS Code"><TextInput value={form.hsCode} onChange={e => setForm({ ...form, hsCode: e.target.value })} /></Field>
          <Field label="HTS Code"><TextInput value={form.htsCode} onChange={e => setForm({ ...form, htsCode: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Country of Origin"><TextInput value={form.countryOfOrigin} onChange={e => setForm({ ...form, countryOfOrigin: e.target.value })} /></Field>
          <Field label="Destination Country"><TextInput value={form.destinationCountry} onChange={e => setForm({ ...form, destinationCountry: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddTariffVersionModal({ open, onClose, ctx, classification }) {
  const blank = { baseDutyPct: '', additionalTariffPct: '', section301Pct: '', antidumpingCvdPct: '', effectiveFrom: todayISO(), effectiveUntil: '', status: 'Active', source: '', notes: '', reason: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open || !classification) return;
    const current = currentTariffVersion(classification);
    setForm({ ...blank, ...(current ? { baseDutyPct: current.baseDutyPct, additionalTariffPct: current.additionalTariffPct, section301Pct: current.section301Pct, antidumpingCvdPct: current.antidumpingCvdPct, source: current.source } : {}) });
  }, [open, classification]);
  if (!classification) return null;
  function submit() {
    if (!form.reason.trim()) return;
    ctx.addTariffVersion(classification.id, {
      baseDutyPct: Number(form.baseDutyPct) || 0, additionalTariffPct: Number(form.additionalTariffPct) || 0,
      section301Pct: Number(form.section301Pct) || 0, antidumpingCvdPct: Number(form.antidumpingCvdPct) || 0,
      effectiveFrom: form.effectiveFrom, effectiveUntil: form.effectiveUntil || null, status: form.status, source: form.source, notes: form.notes,
    }, form.reason);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`New Rate Version — ${classification.htsCode || classification.hsCode}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save New Version</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">This never overwrites the prior rate — it creates a new version effective from the date below. Existing tariff lines keep the rate that was in force when they were created.</p>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Base Duty %"><TextInput type="number" value={form.baseDutyPct} onChange={e => setForm({ ...form, baseDutyPct: e.target.value })} /></Field>
          <Field label="Additional Tariff %"><TextInput type="number" value={form.additionalTariffPct} onChange={e => setForm({ ...form, additionalTariffPct: e.target.value })} /></Field>
          <Field label="Section 301 / Other %"><TextInput type="number" value={form.section301Pct} onChange={e => setForm({ ...form, section301Pct: e.target.value })} /></Field>
          <Field label="Antidumping / CVD %"><TextInput type="number" value={form.antidumpingCvdPct} onChange={e => setForm({ ...form, antidumpingCvdPct: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Effective From"><TextInput type="date" value={form.effectiveFrom} onChange={e => setForm({ ...form, effectiveFrom: e.target.value })} /></Field>
          <Field label="Effective Until (optional)"><TextInput type="date" value={form.effectiveUntil} onChange={e => setForm({ ...form, effectiveUntil: e.target.value })} /></Field>
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{TARIFF_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
        </div>
        <Field label="Source / Reference"><TextInput value={form.source} onChange={e => setForm({ ...form, source: e.target.value })} /></Field>
        <Field label="Reason for Change (required)"><TextArea rows={2} value={form.reason} onChange={e => setForm({ ...form, reason: e.target.value })} placeholder="e.g. Section 301 List 4A rate increase per USTR notice…" /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function TariffClassificationDetailModal({ open, classification, onClose, ctx }) {
  const [showVersion, setShowVersion] = useState(false);
  const [showEdit, setShowEdit] = useState(false);
  if (!classification) return null;
  const current = currentTariffVersion(classification);
  const editable = ctx.canEditTariffClassification;
  const sortedVersions = [...classification.versions].sort((a, b) => (a.effectiveFrom < b.effectiveFrom ? 1 : -1));
  const linesUsingThis = ctx.tariffLines.filter(l => l.tariffClassificationId === classification.id);
  return (
    <Modal open={open} onClose={onClose} wide title={`${classification.htsCode || classification.hsCode} — ${classification.productDescription}`} footer={<><Button variant="ghost" onClick={onClose}>Close</Button>{editable && <Button variant="outline" onClick={() => setShowEdit(true)}>✎ Edit</Button>}</>}>
      <div className="space-y-4">
        <div className="grid sm:grid-cols-3 gap-3 text-xs">
          <Field label="Category"><p className="text-sm">{classification.materialCategory || '—'}</p></Field>
          <Field label="HS Code"><p className="text-sm">{classification.hsCode || '—'}</p></Field>
          <Field label="HTS Code"><p className="text-sm">{classification.htsCode || '—'}</p></Field>
          <Field label="Country of Origin"><p className="text-sm">{classification.countryOfOrigin || '—'}</p></Field>
          <Field label="Destination"><p className="text-sm">{classification.destinationCountry || '—'}</p></Field>
          <Field label="Tariff Lines Using This"><p className="text-sm">{linesUsingThis.length}</p></Field>
        </div>
        {current && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-[var(--leon-cream)]">
            <div className="flex items-center justify-between mb-2">
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">Current Rate (Rev {current.revision})</p>
              <div className="flex items-center gap-2">
                <StatusBadge status={current.status} />
                {editable && <Button size="sm" variant="outline" onClick={() => setShowVersion(true)}>+ Add New Version</Button>}
              </div>
            </div>
            <div className="grid sm:grid-cols-4 gap-2 text-xs mb-2">
              <p>Base Duty: <strong>{fmtPct(current.baseDutyPct)}</strong></p>
              <p>Additional Tariff: <strong>{fmtPct(current.additionalTariffPct)}</strong></p>
              <p>Section 301 / Other: <strong>{fmtPct(current.section301Pct)}</strong></p>
              <p>Antidumping / CVD: <strong>{fmtPct(current.antidumpingCvdPct)}</strong></p>
            </div>
            <p className="text-sm font-bold">Total Estimated Duty: {fmtPct(current.totalEstimatedDutyPct)}</p>
            <p className="text-xs text-[var(--leon-black)]/50 mt-1">Effective {fmtDate(current.effectiveFrom)}{current.effectiveUntil ? ` – ${fmtDate(current.effectiveUntil)}` : ' – present'} · Source: {current.source || '—'} · Last verified {fmtDate(current.lastVerifiedDate)} by {current.verifiedBy}</p>
          </div>
        )}
        <Collapsible title="Revision History" count={sortedVersions.length}>
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Rev</th><th className="py-1">Total Duty %</th><th className="py-1">Effective From</th><th className="py-1">Effective Until</th><th className="py-1">Status</th><th className="py-1">Source</th></tr></thead>
            <tbody>{sortedVersions.map(v => (
              <tr key={v.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 font-semibold">R{v.revision}</td>
                <td className="py-1.5">{fmtPct(v.totalEstimatedDutyPct)}</td>
                <td className="py-1.5">{fmtDate(v.effectiveFrom)}</td>
                <td className="py-1.5">{v.effectiveUntil ? fmtDate(v.effectiveUntil) : 'Present'}</td>
                <td className="py-1.5"><StatusBadge status={v.status} /></td>
                <td className="py-1.5 text-[var(--leon-black)]/50">{v.source || '—'}</td>
              </tr>
            ))}</tbody>
          </table>
        </Collapsible>
        {ctx.canSeeChangeLog && (
          <Collapsible title="Change Log" count={classification.changeLog.length}>
            {classification.changeLog.length === 0 ? <EmptyState text="No changes logged yet." /> : classification.changeLog.map(e => (
              <div key={e.id} className="py-1.5 border-b border-[var(--leon-line)] last:border-0 text-xs">
                <p><strong>{e.field}</strong>: {String(e.previousValue)} → {String(e.newValue)}</p>
                <p className="text-[var(--leon-black)]/50">{e.user} · {fmtDate(e.date.slice(0, 10))} · effective {fmtDate(e.effectiveDate)}{e.reason ? ` — ${e.reason}` : ''}</p>
              </div>
            ))}
          </Collapsible>
        )}
        <Field label="Notes"><p className="text-sm">{classification.notes || '—'}</p></Field>
      </div>
      <AddTariffVersionModal open={showVersion} onClose={() => setShowVersion(false)} ctx={ctx} classification={classification} />
      <EditTariffClassificationModal open={showEdit} onClose={() => setShowEdit(false)} ctx={ctx} classification={classification} />
    </Modal>
  );
}
function TariffLinesSubTab({ ctx }) {
  const [search, setSearch] = useState('');
  const [projectFilter, setProjectFilter] = useState('All');
  const [statusFilter, setStatusFilter] = useState('All');
  const [countryFilter, setCountryFilter] = useState('All');
  const [dateFrom, setDateFrom] = useState('');
  const [dateTo, setDateTo] = useState('');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const all = useMemo(() => enrichedTariffLines(ctx), [ctx.tariffLines, ctx.projects, ctx.vendors, ctx.tariffLibrary]);
  const countries = [...new Set(all.map(l => l.countryOfOrigin).filter(Boolean))].sort();
  const filtered = all.filter(l => {
    if (projectFilter !== 'All' && l.projectId !== projectFilter) return false;
    if (statusFilter !== 'All' && l.status !== statusFilter) return false;
    if (countryFilter !== 'All' && l.countryOfOrigin !== countryFilter) return false;
    if (dateFrom && l.createdDate < dateFrom) return false;
    if (dateTo && l.createdDate > dateTo) return false;
    if (search.trim()) {
      const q = search.trim().toLowerCase();
      if (![l.projectName, l.scopeName, l.vendorName, l.containerNumber, l.hsCode, l.htsCode, l.productDescription, l.brokerName].some(v => (v || '').toLowerCase().includes(q))) return false;
    }
    return true;
  });
  return (
    <div>
      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput placeholder="Search project, scope, vendor, export #, HTS, product…" value={search} onChange={e => setSearch(e.target.value)} className="!w-72 !py-1.5 !text-sm" />
        <Select value={projectFilter} onChange={e => setProjectFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Projects</option>
          {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
        </Select>
        <Select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Statuses</option>
          {TARIFF_LINE_STATUSES.map(s => <option key={s}>{s}</option>)}
        </Select>
        <Select value={countryFilter} onChange={e => setCountryFilter(e.target.value)} className="!w-auto !py-1.5 !text-sm">
          <option value="All">All Countries of Origin</option>
          {countries.map(c => <option key={c}>{c}</option>)}
        </Select>
        <TextInput type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)} className="!w-auto !py-1.5 !text-sm" />
        <span className="text-xs text-[var(--leon-black)]/40">to</span>
        <TextInput type="date" value={dateTo} onChange={e => setDateTo(e.target.value)} className="!w-auto !py-1.5 !text-sm" />
        {ctx.canEditTariffClassification && <Button size="sm" className="ml-auto" onClick={() => setShowAdd(true)}>+ Add Tariff Line</Button>}
      </div>
      <p className="text-xs text-[var(--leon-black)]/40 mb-2">{filtered.length} of {all.length} lines</p>
      {filtered.length === 0 ? <EmptyState text="No tariff lines match." /> : (
        <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase">
              <th className="px-3 py-2">Project</th><th className="px-3 py-2">Scope</th><th className="px-3 py-2">Vendor</th><th className="px-3 py-2">Export #</th>
              <th className="px-3 py-2">Product</th><th className="px-3 py-2">Origin</th><th className="px-3 py-2">HTS/HS</th>
              <th className="px-3 py-2">Customs Value</th><th className="px-3 py-2">Rate %</th><th className="px-3 py-2">Est. Tariff</th>
              <th className="px-3 py-2">Actual Tariff</th><th className="px-3 py-2">Variance</th><th className="px-3 py-2">Status</th>
            </tr></thead>
            <tbody>
              {filtered.map(l => (
                <tr key={l.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => setDetailFor(l)}>
                  <td className="px-3 py-2">{l.projectName}</td>
                  <td className="px-3 py-2">{l.scopeName}</td>
                  <td className="px-3 py-2">{l.vendorName}</td>
                  <td className="px-3 py-2">{l.containerNumber}</td>
                  <td className="px-3 py-2">{l.productDescription || '—'}</td>
                  <td className="px-3 py-2">{l.countryOfOrigin || '—'}</td>
                  <td className="px-3 py-2 font-semibold">{l.htsCode || l.hsCode || '—'}</td>
                  <td className="px-3 py-2">{fmtMoney(l.customsValue)}</td>
                  <td className="px-3 py-2">{fmtPct(l.applicableTariffPct)}</td>
                  <td className="px-3 py-2">{fmtMoney(l.estimatedTariff)}</td>
                  <td className="px-3 py-2">{l.actualTariff !== null ? fmtMoney(l.actualTariff) : '—'}</td>
                  <td className={`px-3 py-2 ${l.varianceAmount > 0 ? 'text-[var(--leon-red)]' : l.varianceAmount < 0 ? 'text-[var(--leon-green)]' : ''}`}>{l.varianceAmount !== null ? fmtMoney(l.varianceAmount) : '—'}</td>
                  <td className="px-3 py-2"><StatusBadge status={l.status} /></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <AddTariffLineModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
      <TariffLineDetailModal open={!!detailFor} line={detailFor ? enrichedTariffLines(ctx).find(l => l.id === detailFor.id) : null} onClose={() => setDetailFor(null)} ctx={ctx} />
    </div>
  );
}
function AddTariffLineModal({ open, onClose, ctx, project: fixedProject, defaultScopeId, defaultContainerId, editLine }) {
  const isEdit = !!editLine;
  const blank = {
    projectId: fixedProject ? fixedProject.id : '', scopeId: defaultScopeId || '', vendorId: '', exportContainerId: defaultContainerId || '',
    tariffClassificationId: '', productDescription: '', countryOfOrigin: '', hsCode: '', htsCode: '',
    customsValue: '', applicableTariffPct: '', notes: '',
  };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (editLine) {
      setForm({
        projectId: editLine.projectId, scopeId: editLine.scopeId || '', vendorId: editLine.vendorId || '', exportContainerId: editLine.exportContainerId || '',
        tariffClassificationId: editLine.tariffClassificationId || '', productDescription: editLine.productDescription, countryOfOrigin: editLine.countryOfOrigin,
        hsCode: editLine.hsCode, htsCode: editLine.htsCode, customsValue: editLine.customsValue, applicableTariffPct: editLine.applicableTariffPct, notes: editLine.notes,
      });
    } else {
      setForm({ ...blank, projectId: fixedProject ? fixedProject.id : '', scopeId: defaultScopeId || '', exportContainerId: defaultContainerId || '' });
    }
  }, [open, fixedProject, defaultScopeId, defaultContainerId, editLine]);
  const project = fixedProject || ctx.projects.find(p => p.id === form.projectId);
  const containers = project ? containersForProject(ctx.exportContainers, project.id) : [];
  function pickClassification(id) {
    const cls = ctx.tariffLibrary.find(c => c.id === id);
    if (!cls) { setForm({ ...form, tariffClassificationId: id }); return; }
    const v = currentTariffVersion(cls);
    setForm({
      ...form, tariffClassificationId: id, productDescription: cls.productDescription, countryOfOrigin: cls.countryOfOrigin,
      hsCode: cls.hsCode, htsCode: cls.htsCode, applicableTariffPct: v ? v.totalEstimatedDutyPct : form.applicableTariffPct,
    });
  }
  function submit() {
    if (!form.projectId || !form.customsValue) return;
    const payload = {
      ...form, exportContainerId: form.exportContainerId || null, tariffClassificationId: form.tariffClassificationId || null,
      vendorId: form.vendorId || null, scopeId: form.scopeId || null,
      customsValue: Number(form.customsValue) || 0, applicableTariffPct: Number(form.applicableTariffPct) || 0,
    };
    if (isEdit) ctx.updateTariffLine(editLine.id, payload, 'Edited tariff line');
    else ctx.addTariffLine(payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={isEdit ? 'Edit Tariff Line' : 'Add Tariff Line'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Tariff Line'}</Button></>}>
      <div className="space-y-3">
        {!fixedProject && (
          <Field label="Project"><Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '', exportContainerId: '' })}><option value="">—</option>{ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
        )}
        <div className="grid grid-cols-3 gap-3">
          <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })} disabled={!project}><option value="">—</option>{project && project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
          <Field label="Vendor"><Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}><option value="">—</option>{ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</Select></Field>
          <Field label="Export Container" hint="Optional — link to a specific shipment"><Select value={form.exportContainerId} onChange={e => setForm({ ...form, exportContainerId: e.target.value })} disabled={!project}><option value="">—</option>{containers.map(c => <option key={c.id} value={c.id}>{c.containerNumber}</option>)}</Select></Field>
        </div>
        <Field label="Tariff Classification" hint="Optional — pulls HTS/rate from the library and locks it to this line">
          <Select value={form.tariffClassificationId} onChange={e => pickClassification(e.target.value)}>
            <option value="">— manual entry —</option>
            {ctx.tariffLibrary.map(c => <option key={c.id} value={c.id}>{c.htsCode || c.hsCode} — {c.productDescription}</option>)}
          </Select>
        </Field>
        <Field label="Product / Material Description"><TextInput value={form.productDescription} onChange={e => setForm({ ...form, productDescription: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="HS Code"><TextInput value={form.hsCode} onChange={e => setForm({ ...form, hsCode: e.target.value })} /></Field>
          <Field label="HTS Code"><TextInput value={form.htsCode} onChange={e => setForm({ ...form, htsCode: e.target.value })} /></Field>
        </div>
        <Field label="Country of Origin"><TextInput value={form.countryOfOrigin} onChange={e => setForm({ ...form, countryOfOrigin: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Customs Value"><TextInput type="number" value={form.customsValue} onChange={e => setForm({ ...form, customsValue: e.target.value })} /></Field>
          <Field label="Applicable Tariff %"><TextInput type="number" value={form.applicableTariffPct} onChange={e => setForm({ ...form, applicableTariffPct: e.target.value })} /></Field>
        </div>
        {form.customsValue && form.applicableTariffPct && (
          <p className="text-xs text-[var(--leon-black)]/50">Estimated Tariff: <strong className="text-[var(--leon-black)]">{fmtMoney((Number(form.customsValue) || 0) * (Number(form.applicableTariffPct) || 0) / 100)}</strong></p>
        )}
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function TariffLineDetailModal({ open, line, onClose, ctx }) {
  const [showActuals, setShowActuals] = useState(false);
  const [showEdit, setShowEdit] = useState(false);
  if (!line) return null;
  const project = ctx.projects.find(p => p.id === line.projectId);
  const canEditFinancial = ctx.canEditTariffClassification;
  return (
    <Modal open={open} onClose={onClose} wide title={`Tariff Line — ${line.productDescription || line.htsCode || line.hsCode}`} footer={<><Button variant="ghost" onClick={onClose}>Close</Button>{canEditFinancial && <Button variant="outline" onClick={() => setShowEdit(true)}>✎ Edit</Button>}</>}>
      <div className="space-y-4">
        <div className="grid sm:grid-cols-3 gap-3 text-xs">
          <Field label="Project"><button onClick={() => project && ctx.goProjectTab(project.id, 'export')} className="text-sm text-[var(--leon-brown)] font-semibold hover:underline">{line.projectName}</button></Field>
          <Field label="Scope"><p className="text-sm">{line.scopeName}</p></Field>
          <Field label="Vendor"><p className="text-sm">{line.vendorName}</p></Field>
          <Field label="Export Container"><p className="text-sm">{line.containerNumber}</p></Field>
          <Field label="Country of Origin"><p className="text-sm">{line.countryOfOrigin || '—'}</p></Field>
          <Field label="HTS / HS Code"><p className="text-sm font-semibold">{line.htsCode || line.hsCode || '—'}</p></Field>
        </div>
        <div className="grid sm:grid-cols-4 gap-3 border border-[var(--leon-line)] rounded-lg p-3 bg-[var(--leon-cream)]">
          <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Customs Value</p><p className="text-sm font-bold">{fmtMoney(line.customsValue)}</p></div>
          <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Applicable Rate</p><p className="text-sm font-bold">{fmtPct(line.applicableTariffPct)}</p></div>
          <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Estimated Tariff</p><p className="text-sm font-bold">{fmtMoney(line.estimatedTariff)}</p></div>
          <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Actual Tariff</p><p className="text-sm font-bold">{line.actualTariff !== null ? fmtMoney(line.actualTariff) : '—'}</p></div>
          {line.varianceAmount !== null && (
            <>
              <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Variance $</p><p className={`text-sm font-bold ${line.varianceAmount > 0 ? 'text-[var(--leon-red)]' : 'text-[var(--leon-green)]'}`}>{fmtMoney(line.varianceAmount)}</p></div>
              <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Variance %</p><p className={`text-sm font-bold ${line.variancePct > 0 ? 'text-[var(--leon-red)]' : 'text-[var(--leon-green)]'}`}>{fmtPct(line.variancePct)}</p></div>
            </>
          )}
          <div><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Status</p><StatusBadge status={line.status} /></div>
        </div>
        {line.actual ? (
          <Collapsible title="Actual Customs Charges" count={fmtMoney(line.actual.totalCustomsCost)}>
            <div className="grid sm:grid-cols-3 gap-2 text-xs">
              <p>Entry #: <strong>{line.actual.customsEntryNumber || '—'}</strong></p>
              <p>Broker: <strong>{line.brokerName}</strong></p>
              <p>Entry Date: <strong>{fmtDate(line.actual.entryDate)}</strong></p>
              <p>Declared HTS: <strong>{line.actual.declaredHtsCode || '—'}</strong></p>
              <p>Entered Value: <strong>{fmtMoney(line.actual.enteredValue)}</strong></p>
              <p>Base Duty: <strong>{fmtMoney(line.actual.baseDuty)}</strong></p>
              <p>Additional Tariff: <strong>{fmtMoney(line.actual.additionalTariff)}</strong></p>
              <p>Section 301: <strong>{fmtMoney(line.actual.section301)}</strong></p>
              <p>Antidumping/CVD: <strong>{fmtMoney(line.actual.antidumpingCvd)}</strong></p>
              <p>MPF: <strong>{fmtMoney(line.actual.merchandiseProcessingFee)}</strong></p>
              <p>HMF: <strong>{fmtMoney(line.actual.harborMaintenanceFee)}</strong></p>
              <p>Brokerage Fees: <strong>{fmtMoney(line.actual.brokerageFees)}</strong></p>
              <p>Other Charges: <strong>{fmtMoney(line.actual.otherCharges)}</strong></p>
            </div>
            <div className="flex gap-3 mt-2">
              <FileField name={line.actual.brokerInvoiceFile} url={line.actual.brokerInvoiceFileUrl} editable={false} onChange={() => {}} placeholder="No broker invoice" />
              <FileField name={line.actual.entrySummaryFile} url={line.actual.entrySummaryFileUrl} editable={false} onChange={() => {}} placeholder="No entry summary" />
            </div>
            {canEditFinancial && <Button size="sm" variant="ghost" className="mt-2" onClick={() => setShowActuals(true)}>✎ Edit Actual Customs Charges</Button>}
          </Collapsible>
        ) : ctx.canEditTariffShipmentInfo ? (
          <Button size="sm" variant="outline" onClick={() => setShowActuals(true)}>+ Record Actual Customs Charges</Button>
        ) : null}
        {ctx.canSeeChangeLog && (
          <Collapsible title="Change Log" count={line.changeLog.length}>
            {line.changeLog.length === 0 ? <EmptyState text="No changes logged yet." /> : line.changeLog.map(e => (
              <div key={e.id} className="py-1.5 border-b border-[var(--leon-line)] last:border-0 text-xs">
                <p><strong>{e.field}</strong>: {String(e.previousValue)} → {String(e.newValue)}</p>
                <p className="text-[var(--leon-black)]/50">{e.user} · {fmtDate(e.date.slice(0, 10))}{e.reason ? ` — ${e.reason}` : ''}</p>
              </div>
            ))}
          </Collapsible>
        )}
        <Field label="Notes"><p className="text-sm">{line.notes || '—'}</p></Field>
      </div>
      <RecordTariffActualsModal open={showActuals} onClose={() => setShowActuals(false)} ctx={ctx} line={line} />
      <AddTariffLineModal open={showEdit} onClose={() => setShowEdit(false)} ctx={ctx} editLine={line} />
    </Modal>
  );
}
function RecordTariffActualsModal({ open, onClose, ctx, line }) {
  const blank = {
    customsEntryNumber: '', brokerId: '', entryDate: todayISO(), declaredHtsCode: '', enteredValue: '',
    baseDuty: '', additionalTariff: '', section301: '', antidumpingCvd: '',
    merchandiseProcessingFee: '', harborMaintenanceFee: '', brokerageFees: '', otherCharges: '',
    brokerInvoiceFile: '', brokerInvoiceFileUrl: null, entrySummaryFile: '', entrySummaryFileUrl: null, reason: '',
  };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open || !line) return;
    setForm(line.actual ? { ...blank, ...line.actual, reason: '' } : { ...blank, declaredHtsCode: line.htsCode || line.hsCode, enteredValue: line.customsValue });
  }, [open, line]);
  if (!line) return null;
  function submit() {
    if (!form.customsEntryNumber.trim()) return;
    ctx.recordTariffActuals(line.id, form, form.reason);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Record Actual Customs Charges" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Actual Charges</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Customs Entry Number"><TextInput value={form.customsEntryNumber} onChange={e => setForm({ ...form, customsEntryNumber: e.target.value })} /></Field>
          <Field label="Broker"><Select value={form.brokerId} onChange={e => setForm({ ...form, brokerId: e.target.value })}><option value="">—</option>{ctx.freightForwarders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}</Select></Field>
          <Field label="Customs Entry Date"><TextInput type="date" value={form.entryDate} onChange={e => setForm({ ...form, entryDate: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Declared HTS Code"><TextInput value={form.declaredHtsCode} onChange={e => setForm({ ...form, declaredHtsCode: e.target.value })} /></Field>
          <Field label="Entered / Customs Value"><TextInput type="number" value={form.enteredValue} onChange={e => setForm({ ...form, enteredValue: e.target.value })} disabled={!ctx.canEditTariffClassification} /></Field>
        </div>
        <fieldset disabled={!ctx.canEditTariffClassification}>
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 pt-1 mb-1">Duties (financial — restricted to Admin/Accounting/Logistic Manager/Export Manager)</p>
          <div className="grid grid-cols-4 gap-3">
            <Field label="Base Duty"><TextInput type="number" value={form.baseDuty} onChange={e => setForm({ ...form, baseDuty: e.target.value })} /></Field>
            <Field label="Additional Tariff"><TextInput type="number" value={form.additionalTariff} onChange={e => setForm({ ...form, additionalTariff: e.target.value })} /></Field>
            <Field label="Section 301 Duties"><TextInput type="number" value={form.section301} onChange={e => setForm({ ...form, section301: e.target.value })} /></Field>
            <Field label="Antidumping / CVD"><TextInput type="number" value={form.antidumpingCvd} onChange={e => setForm({ ...form, antidumpingCvd: e.target.value })} /></Field>
          </div>
          <div className="grid grid-cols-4 gap-3 mt-3">
            <Field label="Merchandise Processing Fee"><TextInput type="number" value={form.merchandiseProcessingFee} onChange={e => setForm({ ...form, merchandiseProcessingFee: e.target.value })} /></Field>
            <Field label="Harbor Maintenance Fee"><TextInput type="number" value={form.harborMaintenanceFee} onChange={e => setForm({ ...form, harborMaintenanceFee: e.target.value })} /></Field>
            <Field label="Brokerage Fees"><TextInput type="number" value={form.brokerageFees} onChange={e => setForm({ ...form, brokerageFees: e.target.value })} /></Field>
            <Field label="Other Customs Charges"><TextInput type="number" value={form.otherCharges} onChange={e => setForm({ ...form, otherCharges: e.target.value })} /></Field>
          </div>
        </fieldset>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Broker Invoice"><FileField name={form.brokerInvoiceFile} url={form.brokerInvoiceFileUrl} onChange={(fname, url) => setForm({ ...form, brokerInvoiceFile: fname, brokerInvoiceFileUrl: url })} editable /></Field>
          <Field label="Customs Entry Summary"><FileField name={form.entrySummaryFile} url={form.entrySummaryFileUrl} onChange={(fname, url) => setForm({ ...form, entrySummaryFile: fname, entrySummaryFileUrl: url })} editable /></Field>
        </div>
        <Field label="Reason / Note for this entry"><TextArea rows={2} value={form.reason} onChange={e => setForm({ ...form, reason: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function TariffExposureSubTab({ ctx, onNavigate }) {
  const lines = useMemo(() => enrichedTariffLines(ctx), [ctx.tariffLines, ctx.projects, ctx.vendors, ctx.tariffLibrary]);
  const open = lines.filter(l => l.status !== 'Cleared');
  const totalOpenValue = open.reduce((s, l) => s + (l.customsValue || 0), 0);
  const totalExposure = open.reduce((s, l) => s + (l.estimatedTariff || 0), 0);
  const importedValue = lines.filter(l => l.status === 'Cleared').reduce((s, l) => s + (l.customsValue || 0), 0);

  // Rate-change impact: for each open line linked to a classification, check
  // whether the classification's CURRENT version's total % differs from the
  // rate the line locked in — if so, the line is exposed to the rate change.
  const affected = open.filter(l => l.tariffClassificationId).map(l => {
    const cls = ctx.tariffLibrary.find(c => c.id === l.tariffClassificationId);
    const current = cls ? currentTariffVersion(cls) : null;
    if (!current || current.totalEstimatedDutyPct === l.applicableTariffPct) return null;
    const newEstimate = (l.customsValue * current.totalEstimatedDutyPct) / 100;
    return { ...l, currentRatePct: current.totalEstimatedDutyPct, additionalExposure: newEstimate - l.estimatedTariff };
  }).filter(Boolean);
  const affectedProjects = new Set(affected.map(l => l.projectId)).size;
  const totalAdditionalExposure = affected.reduce((s, l) => s + l.additionalExposure, 0);

  return (
    <div>
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
        <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-white"><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Total Open Procurement Value</p><p className="text-lg font-bold">{fmtMoney(totalOpenValue)}</p></div>
        <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-white"><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Estimated Tariff Exposure</p><p className="text-lg font-bold text-[var(--leon-red)]">{fmtMoney(totalExposure)}</p></div>
        <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-white"><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Material Already Imported</p><p className="text-lg font-bold">{fmtMoney(importedValue)}</p></div>
        <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-white"><p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">Remaining Unshipped Value</p><p className="text-lg font-bold">{fmtMoney(totalOpenValue)}</p></div>
      </div>

      {affected.length > 0 && (
        <div className="border border-[var(--leon-red)]/40 bg-[#fbe7e7] rounded-lg p-3 mb-4">
          <p className="text-sm font-bold text-[var(--leon-red)]">⚠ Tariff classification rate changes affect open procurement</p>
          <p className="text-xs text-[var(--leon-black)]/70 mt-1">{affected.length} tariff line{affected.length === 1 ? '' : 's'} across {affectedProjects} project{affectedProjects === 1 ? '' : 's'} were locked in at a rate that no longer matches the current library rate. Estimated additional tariff exposure: <strong>{fmtMoney(totalAdditionalExposure)}</strong>.</p>
        </div>
      )}
      <Collapsible title="Open Lines Affected by a Rate Change" count={affected.length}>
        {affected.length === 0 ? <EmptyState text="No open lines are affected by a recent rate change." /> : (
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Project</th><th className="py-1">Scope</th><th className="py-1">HTS/HS</th><th className="py-1">Locked Rate</th><th className="py-1">Current Rate</th><th className="py-1">Additional Exposure</th></tr></thead>
            <tbody>{affected.map(l => (
              <tr key={l.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => onNavigate('lines')}>
                <td className="py-1.5">{l.projectName}</td>
                <td className="py-1.5">{l.scopeName}</td>
                <td className="py-1.5 font-semibold">{l.htsCode || l.hsCode}</td>
                <td className="py-1.5">{fmtPct(l.applicableTariffPct)}</td>
                <td className="py-1.5">{fmtPct(l.currentRatePct)}</td>
                <td className="py-1.5 text-[var(--leon-red)] font-semibold">{fmtMoney(l.additionalExposure)}</td>
              </tr>
            ))}</tbody>
          </table>
        )}
      </Collapsible>
      <Collapsible title="All Open (Unshipped) Tariff Lines" count={open.length}>
        {open.length === 0 ? <EmptyState text="Nothing open." /> : (
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Project</th><th className="py-1">Scope</th><th className="py-1">Vendor</th><th className="py-1">Customs Value</th><th className="py-1">Est. Tariff</th><th className="py-1">Status</th></tr></thead>
            <tbody>{open.map(l => (
              <tr key={l.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => onNavigate('lines')}>
                <td className="py-1.5">{l.projectName}</td>
                <td className="py-1.5">{l.scopeName}</td>
                <td className="py-1.5">{l.vendorName}</td>
                <td className="py-1.5">{fmtMoney(l.customsValue)}</td>
                <td className="py-1.5">{fmtMoney(l.estimatedTariff)}</td>
                <td className="py-1.5"><StatusBadge status={l.status} /></td>
              </tr>
            ))}</tbody>
          </table>
        )}
      </Collapsible>
    </div>
  );
}
function RiskBadge({ status }) {
  const tone = status === 'Completed' ? 'green' : status === 'On Track' ? 'green' : status === 'Attention Required' ? 'yellow' : 'red';
  return <Badge tone={tone}>{status}</Badge>;
}

// ============================================================================
// Warehouse Hub — physical inventory (imported from the Monday.com "Asset
// stock" board) plus the new Material Allocation workflow. Allocation
// permission (Sales/Admin/Logistics) is deliberately separate from physical
// inventory control (Admin/Logistic Manager for sensitive actions) — see
// canAllocateMaterial/canControlInventory/canAssistWarehouse, data.jsx.
// ============================================================================
const WAREHOUSE_SUBTABS = [
  { key: 'inventory', label: 'Inventory', icon: '📦' },
  { key: 'allocations', label: 'Allocations', icon: '🔗' },
  { key: 'releases', label: 'Releases & Packing Lists', icon: '📤' },
  { key: 'receiving', label: 'Receiving & Adjustments', icon: '📥' },
  { key: 'validation', label: 'Import Validation', icon: '🔎' },
];
// `embedded` drops the page title when a hub above already supplies one — the
// same prop MeetTheTeamView and UsersView take, for the same reason.
function WarehouseHubTab({ ctx, embedded }) {
  const [sub, setSub] = useState('inventory');
  return (
    <div>
      {!embedded && (
        <div className="mb-5">
          <h1 className="text-2xl font-bold">Inventory</h1>
          <p className="text-sm text-[var(--leon-black)]/50">Physical inventory, material allocation to projects, and receiving/adjustments — one connected stock record.</p>
        </div>
      )}
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)]">
        {WAREHOUSE_SUBTABS.filter(t => t.key !== 'validation' || ctx.currentRole === 'Admin').map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      {sub === 'inventory' && <WarehouseInventorySubTab ctx={ctx} />}
      {sub === 'allocations' && <WarehouseAllocationsSubTab ctx={ctx} />}
      {sub === 'releases' && <WarehouseReleasesSubTab ctx={ctx} />}
      {sub === 'receiving' && <WarehouseReceivingSubTab ctx={ctx} />}
      {sub === 'validation' && ctx.currentRole === 'Admin' && <ImportValidationSubTab ctx={ctx} />}
    </div>
  );
}

// Item icon for an inventory material: its first photo where one exists,
// otherwise a neutral tile carrying the material's initials so the column
// never collapses and rows stay the same height.
function MaterialThumb({ material, onClick, size = 34 }) {
  const pic = (material.pictures || []).find(Boolean);
  const initials = (material.name || '?').split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase();
  const common = 'rounded border border-[var(--leon-line)] object-cover shrink-0';
  if (pic) {
    return (
      <button type="button" onClick={onClick} title={material.name} className="block cursor-pointer">
        <img src={pic} alt="" loading="lazy" className={common} style={{ width: size, height: size }} />
      </button>
    );
  }
  return (
    <button type="button" onClick={onClick} title={material.name}
      className={`${common} bg-[var(--leon-cream)] text-[10px] font-bold text-[var(--leon-black)]/35 flex items-center justify-center`}
      style={{ width: size, height: size }}>
      {initials}
    </button>
  );
}

function WarehouseInventorySubTab({ ctx }) {
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('All');
  const [adjustFor, setAdjustFor] = useState(null);
  const [receiveFor, setReceiveFor] = useState(null);
  const [historyFor, setHistoryFor] = useState(null);
  const [editFor, setEditFor] = useState(null);
  const [addOpen, setAddOpen] = useState(false);
  const [importOpen, setImportOpen] = useState(false);
  // Alphabetical rather than whatever order the materials happen to sit in —
  // the tab you want should be findable by name, not by remembering the import.
  const categories = ['All', ...Array.from(new Set(ctx.warehouseMaterials.map(m => m.category))).sort((a, b) => (a || '').localeCompare(b || ''))];
  const filtered = ctx.warehouseMaterials.filter(m =>
    (category === 'All' || m.category === category) &&
    (!search || m.name.toLowerCase().includes(search.toLowerCase()))
  );
  return (
    <div>
      <div className="flex items-center justify-end gap-2 mb-3">
        <TextInput placeholder="Search materials…" value={search} onChange={e => setSearch(e.target.value)} className="!w-52" />
        {ctx.canCreateInventory && <Button variant="outline" onClick={() => setImportOpen(true)}>&#11014; Import Inventory</Button>}
        {ctx.canCreateInventory && <Button onClick={() => setAddOpen(true)}>+ New Material</Button>}
      </div>
      <ImportInventoryModal open={importOpen} onClose={() => setImportOpen(false)} ctx={ctx} />
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {categories.map(c => (
          <button key={c} onClick={() => setCategory(c)} className={`px-3 py-2 text-sm font-semibold whitespace-nowrap border-b-2 ${category === c ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{c}</button>
        ))}
      </div>
      <HubTools />
      <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
        <table className="w-full text-xs">
          <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase"><th className="px-3 py-2 w-12"></th><th className="px-3 py-2">Material</th><th className="px-3 py-2">Category</th><th className="px-3 py-2">Ref #</th><th className="px-3 py-2">Current Stock</th><th className="px-3 py-2">Allocated</th><th className="px-3 py-2">Available</th>{ctx.canSeeFin && <th className="px-3 py-2">Unit Cost</th>}{ctx.canSeeFin && <th className="px-3 py-2">Total Value</th>}<th className="px-3 py-2">Location</th><th className="px-3 py-2"></th></tr></thead>
          <tbody>
            {filtered.map(m => {
              const avail = availableQuantity(m, ctx.materialAllocations);
              return (
                <tr key={m.id} className="border-t border-[var(--leon-line)]">
                  {/* A material's first uploaded photo doubles as its icon —
                      stock is identified by sight on the warehouse floor, so a
                      thumbnail beats reading a name off a list. */}
                  <td className="px-3 py-1.5">
                    <MaterialThumb material={m} onClick={() => setHistoryFor(m)} />
                  </td>
                  <td className="px-3 py-2 font-semibold cursor-pointer hover:underline" onClick={() => setHistoryFor(m)}>{m.name}</td>
                  <td className="px-3 py-2">{m.category}</td>
                  <td className="px-3 py-2">{m.referenceNumber ?? '—'}</td>
                  <td className="px-3 py-2">{m.currentStock} {m.unitOfMeasure}</td>
                  <td className="px-3 py-2">{m.currentStock - avail} {m.unitOfMeasure}</td>
                  <td className="px-3 py-2 font-semibold">{avail < 0 ? <span className="text-[var(--leon-red)]">{avail}</span> : avail} {m.unitOfMeasure}</td>
                  {ctx.canSeeFin && <td className="px-3 py-2">{fmtMoney(m.unitCost)}</td>}
                  {ctx.canSeeFin && <td className="px-3 py-2 font-semibold">{fmtMoney(m.currentStock * m.unitCost)}</td>}
                  <td className="px-3 py-2">{m.storageLocation || '—'}</td>
                  <td className="px-3 py-2 whitespace-nowrap">
                    {ctx.canControlInventory && <Button size="sm" variant="ghost" onClick={() => setEditFor(m)}>Edit</Button>}
                    {ctx.canAssistWarehouse && <Button size="sm" variant="ghost" onClick={() => setReceiveFor(m)}>Receive</Button>}
                    {ctx.canControlInventory && <Button size="sm" variant="ghost" onClick={() => setAdjustFor(m)}>Adjust</Button>}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <ReceiveInventoryModal open={!!receiveFor} material={receiveFor} onClose={() => setReceiveFor(null)} ctx={ctx} />
      <AdjustStockModal open={!!adjustFor} material={adjustFor} onClose={() => setAdjustFor(null)} ctx={ctx} />
      <MaterialDetailModal open={!!historyFor} material={historyFor} onClose={() => setHistoryFor(null)} ctx={ctx} />
      <EditWarehouseMaterialModal open={!!editFor} material={editFor} onClose={() => setEditFor(null)} ctx={ctx} />
      <AddWarehouseMaterialModal open={addOpen} onClose={() => setAddOpen(false)} ctx={ctx} />
    </div>
  );
}
// Fixes a material's own descriptive record (name, category, SKU,
// description, etc.) — separate from Receive (adds stock) and Adjust
// (corrects the stock count with a logged reason), neither of which touch
// these fields. Wired to ctx.updateWarehouseMaterial, which already existed
// but had no caller until now.
function EditWarehouseMaterialModal({ open, material, onClose, ctx }) {
  const blank = { name: '', category: '', itemId: '', manufacturerVendor: '', description: '', finishColor: '', dimensions: '', unitOfMeasure: 'Units', unitCost: '', warehouseId: '', storageLocation: '', notes: '', pictures: [] };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (open && material) setForm({
      name: material.name || '', category: material.category || '', itemId: material.itemId || '', manufacturerVendor: material.manufacturerVendor || '',
      description: material.description || '', finishColor: material.finishColor || '', dimensions: material.dimensions || '', unitOfMeasure: material.unitOfMeasure || 'Units',
      unitCost: material.unitCost ?? '', warehouseId: material.warehouseId || '', storageLocation: material.storageLocation || '', notes: material.notes || '', pictures: material.pictures || [],
    });
  }, [open, material]);
  if (!material) return null;
  function addPicture(url) { setForm({ ...form, pictures: [...form.pictures, url] }); }
  function removePicture(i) { setForm({ ...form, pictures: form.pictures.filter((_, idx) => idx !== i) }); }
  function submit() {
    if (!form.name.trim() || !form.category.trim()) return;
    ctx.updateWarehouseMaterial(material.id, { ...form, unitCost: Number(form.unitCost) || 0 });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Edit — ${material.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Material / Item Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
          <Field label="Category"><TextInput value={form.category} onChange={e => setForm({ ...form, category: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Item ID / SKU"><TextInput value={form.itemId} onChange={e => setForm({ ...form, itemId: e.target.value })} /></Field>
          <Field label="Manufacturer / Vendor"><TextInput value={form.manufacturerVendor} onChange={e => setForm({ ...form, manufacturerVendor: e.target.value })} /></Field>
        </div>
        <Field label="Description"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Finish / Color"><TextInput value={form.finishColor} onChange={e => setForm({ ...form, finishColor: e.target.value })} /></Field>
          <Field label="Dimensions / Specifications"><TextInput value={form.dimensions} onChange={e => setForm({ ...form, dimensions: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Unit of Measure"><TextInput value={form.unitOfMeasure} onChange={e => setForm({ ...form, unitOfMeasure: e.target.value })} /></Field>
          <Field label="Unit Cost"><TextInput type="number" value={form.unitCost} onChange={e => setForm({ ...form, unitCost: e.target.value })} /></Field>
          <Field label="Storage Location"><TextInput value={form.storageLocation} onChange={e => setForm({ ...form, storageLocation: e.target.value })} /></Field>
        </div>
        <Field label="Warehouse"><Select value={form.warehouseId} onChange={e => setForm({ ...form, warehouseId: e.target.value })}>{ctx.warehouses.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}</Select></Field>
        <div>
          <p className="text-xs font-semibold mb-1">Pictures</p>
          <div className="flex items-center gap-2 flex-wrap">
            {form.pictures.map((p, i) => (
              <span key={i} className="relative">
                <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                <button type="button" onClick={() => removePicture(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
              </span>
            ))}
            <ImagePicker url={null} onChange={addPicture} size={56} />
          </div>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function MaterialDetailModal({ open, material, onClose, ctx }) {
  if (!material) return null;
  const tx = ctx.inventoryTransactions.filter(t => t.materialId === material.id).sort((a, b) => (a.date < b.date ? 1 : -1));
  const avail = availableQuantity(material, ctx.materialAllocations);
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Material — ${material.name}`}
      fields={[
        { label: 'Item ID / SKU', value: material.itemId || material.referenceNumber }, { label: 'Category', value: material.category },
        { label: 'Description', value: material.description }, { label: 'Manufacturer / Vendor', value: material.manufacturerVendor },
        { label: 'Finish / Color', value: material.finishColor }, { label: 'Dimensions', value: material.dimensions },
        { label: 'Current Stock', value: `${material.currentStock} ${material.unitOfMeasure}` }, { label: 'Available', value: `${avail} ${material.unitOfMeasure}` },
        ...(ctx.canSeeFin ? [{ label: 'Unit Cost', value: fmtMoney(material.unitCost) }, { label: 'Total Inventory Value', value: fmtMoney(material.currentStock * material.unitCost) }] : []),
        { label: 'Warehouse Location', value: material.storageLocation }, { label: 'Date Received', value: material.dateReceived ? fmtDate(material.dateReceived) : '—' },
        { label: 'Created By', value: material.createdBy }, { label: 'Created Date', value: material.createdDate ? fmtDate(material.createdDate) : '—' },
        { label: 'Notes', value: material.notes },
      ]}
      attachments={[...(material.pictures || []).map((p, i) => ({ name: `Photo ${i + 1}`, url: p })), ...(material.documents || [])]}
    >
      <div>
        <p className="text-xs font-semibold mb-1">Transaction History</p>
        {tx.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40">No transactions recorded.</p> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-2">Date</th><th className="py-1 pr-2">Type</th><th className="py-1 pr-2">Qty</th><th className="py-1 pr-2">Status</th><th className="py-1 pr-2">Recipient / Company</th><th className="py-1 pr-2">Notes</th></tr></thead>
              <tbody>
                {tx.map(t => (
                  <tr key={t.id} className="border-t border-[var(--leon-line)]">
                    <td className="py-1.5 pr-2">{fmtDate(t.date)}</td>
                    <td className="py-1.5 pr-2"><StatusBadge status={t.type} /></td>
                    <td className={`py-1.5 pr-2 font-semibold ${t.quantity < 0 ? 'text-[var(--leon-red)]' : 'text-[var(--leon-green)]'}`}>{t.quantity > 0 ? '+' : ''}{t.quantity}</td>
                    <td className="py-1.5 pr-2">{t.status}</td>
                    <td className="py-1.5 pr-2">{t.recipientText || t.company || '—'}</td>
                    <td className="py-1.5 pr-2">{t.notes || '—'}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
      <div className="mt-3">
        <p className="text-xs font-semibold mb-1">Activity Log</p>
        {(!material.activityLog || material.activityLog.length === 0) ? <p className="text-xs text-[var(--leon-black)]/40">No activity yet.</p> : (
          <div className="space-y-1">
            {[...material.activityLog].reverse().map(a => (
              <p key={a.id} className="text-xs text-[var(--leon-black)]/60">{fmtDate(a.date)} {a.time} — {a.user}: {a.action}{a.newValue ? ` (${a.newValue})` : ''}</p>
            ))}
          </div>
        )}
      </div>
    </RecordDetailModal>
  );
}
function AddWarehouseMaterialModal({ open, onClose, ctx }) {
  const blank = {
    name: '', category: '', itemId: '', description: '', manufacturerVendor: '', finishColor: '', dimensions: '',
    unitOfMeasure: 'Units', currentStock: '', unitCost: '', warehouseId: '', storageLocation: '', dateReceived: todayISO(),
    relatedPoId: '', notes: '', pictures: [], documents: [],
  };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, warehouseId: ctx.warehouses[0]?.id || '' }); }, [open]);
  function addPicture(url) { setForm({ ...form, pictures: [...form.pictures, url] }); }
  function removePicture(i) { setForm({ ...form, pictures: form.pictures.filter((_, idx) => idx !== i) }); }
  function submit() {
    if (!form.name.trim() || !form.category.trim()) return;
    ctx.addWarehouseMaterial({ ...form, currentStock: Number(form.currentStock) || 0, unitCost: Number(form.unitCost) || 0, relatedPoId: form.relatedPoId || null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="New Material" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create Material</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Material / Item Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
          <Field label="Category"><TextInput value={form.category} onChange={e => setForm({ ...form, category: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Item ID / SKU"><TextInput value={form.itemId} onChange={e => setForm({ ...form, itemId: e.target.value })} /></Field>
          <Field label="Manufacturer / Vendor"><TextInput value={form.manufacturerVendor} onChange={e => setForm({ ...form, manufacturerVendor: e.target.value })} /></Field>
        </div>
        <Field label="Description"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Finish / Color"><TextInput value={form.finishColor} onChange={e => setForm({ ...form, finishColor: e.target.value })} /></Field>
          <Field label="Dimensions / Specifications"><TextInput value={form.dimensions} onChange={e => setForm({ ...form, dimensions: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Unit of Measure"><TextInput value={form.unitOfMeasure} onChange={e => setForm({ ...form, unitOfMeasure: e.target.value })} /></Field>
          <Field label="Quantity in Stock"><TextInput type="number" value={form.currentStock} onChange={e => setForm({ ...form, currentStock: e.target.value })} /></Field>
          <Field label="Unit Cost"><TextInput type="number" value={form.unitCost} onChange={e => setForm({ ...form, unitCost: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Warehouse"><Select value={form.warehouseId} onChange={e => setForm({ ...form, warehouseId: e.target.value })}>{ctx.warehouses.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}</Select></Field>
          <Field label="Storage Location"><TextInput value={form.storageLocation} onChange={e => setForm({ ...form, storageLocation: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date Received"><TextInput type="date" value={form.dateReceived} onChange={e => setForm({ ...form, dateReceived: e.target.value })} /></Field>
          <Field label="Related PO # (optional)"><TextInput value={form.relatedPoId} onChange={e => setForm({ ...form, relatedPoId: e.target.value })} placeholder="PO number or reference" /></Field>
        </div>
        <div>
          <p className="text-xs font-semibold mb-1">Pictures</p>
          <div className="flex items-center gap-2 flex-wrap">
            {form.pictures.map((p, i) => (
              <span key={i} className="relative">
                <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                <button type="button" onClick={() => removePicture(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
              </span>
            ))}
            <ImagePicker url={null} onChange={addPicture} size={56} />
          </div>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function ReceiveInventoryModal({ open, material, onClose, ctx }) {
  const blank = { quantity: '', expectedQuantity: '', damagedQuantity: '', date: todayISO(), company: '', po: '', invoiceNumber: '', projectId: '', scopeId: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open, material]);
  if (!material) return null;
  const project = ctx.projects.find(p => p.id === form.projectId);
  function submit() {
    if (!form.quantity) return;
    const expected = form.expectedQuantity === '' ? null : Number(form.expectedQuantity);
    const received = Number(form.quantity) || 0;
    ctx.receiveInventory(material.id, {
      ...form,
      expectedQuantity: expected,
      shortQuantity: expected !== null && received < expected ? expected - received : 0,
      overQuantity: expected !== null && received > expected ? received - expected : 0,
      damagedQuantity: Number(form.damagedQuantity) || 0,
      projectId: form.projectId || null, scopeId: form.scopeId || null,
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Receive Inventory — ${material.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Receive</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Expected Quantity" hint="Optional — enables the Receiving Report's short/over columns"><TextInput type="number" value={form.expectedQuantity} onChange={e => setForm({ ...form, expectedQuantity: e.target.value })} /></Field>
          <Field label={`Received Quantity (${material.unitOfMeasure})`}><TextInput type="number" value={form.quantity} onChange={e => setForm({ ...form, quantity: e.target.value })} /></Field>
          <Field label="Damaged Quantity"><TextInput type="number" value={form.damagedQuantity} onChange={e => setForm({ ...form, damagedQuantity: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Vendor / Company"><TextInput value={form.company} onChange={e => setForm({ ...form, company: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="PO #"><TextInput value={form.po} onChange={e => setForm({ ...form, po: e.target.value })} /></Field>
          <Field label="Invoice #"><TextInput value={form.invoiceNumber} onChange={e => setForm({ ...form, invoiceNumber: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Project" hint="Optional — links this receipt to a project/scope">
            <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>
              <option value="">— none —</option>
              {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <Field label="Scope">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })} disabled={!project}>
              <option value="">— none —</option>
              {project && project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AdjustStockModal({ open, material, onClose, ctx }) {
  const [qty, setQty] = useState('');
  const [reason, setReason] = useState('');
  useEffect(() => { if (open && material) { setQty(String(material.currentStock)); setReason(''); } }, [open, material]);
  if (!material) return null;
  function submit() {
    if (qty === '' || !reason.trim()) return;
    ctx.adjustStock(material.id, Number(qty), reason);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Adjust Stock — ${material.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit}>Save Adjustment</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Current stock: <strong>{material.currentStock} {material.unitOfMeasure}</strong>. Sensitive action — logged with your name and reason to the transaction history.</p>
        <Field label="New Physical Count"><TextInput type="number" value={qty} onChange={e => setQty(e.target.value)} /></Field>
        <Field label="Reason (required)"><TextArea rows={2} value={reason} onChange={e => setReason(e.target.value)} placeholder="e.g. Physical stock check, damaged goods removed, correction…" /></Field>
      </div>
    </Modal>
  );
}

function WarehouseReceivingSubTab({ ctx }) {
  const [showClaim, setShowClaim] = useState(false);
  const sorted = [...ctx.inventoryTransactions].sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 100);
  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
        <p className="text-xs text-[var(--leon-black)]/50">Use "Receive" or "Adjust" from the Inventory tab against a specific material. Most recent 100 transactions across all materials:</p>
        <Button size="sm" variant="ghost" onClick={() => setShowClaim(true)}>⚠ Log Shortage/Damage Claim</Button>
      </div>
      <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
        <table className="w-full text-xs">
          <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase"><th className="px-3 py-2">Date</th><th className="px-3 py-2">Material</th><th className="px-3 py-2">Type</th><th className="px-3 py-2">Qty</th><th className="px-3 py-2">Status</th><th className="px-3 py-2">Entered By</th></tr></thead>
          <tbody>
            {sorted.map(t => {
              const m = ctx.warehouseMaterials.find(x => x.id === t.materialId);
              return (
                <tr key={t.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-3 py-2">{fmtDate(t.date)}</td>
                  <td className="px-3 py-2 font-semibold">{m ? m.name : '—'}</td>
                  <td className="px-3 py-2"><StatusBadge status={t.type} /></td>
                  <td className={`px-3 py-2 font-semibold ${t.quantity < 0 ? 'text-[var(--leon-red)]' : 'text-[var(--leon-green)]'}`}>{t.quantity > 0 ? '+' : ''}{t.quantity}</td>
                  <td className="px-3 py-2">{t.status}</td>
                  <td className="px-3 py-2">{t.enteredBy || '—'}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <AddLogisticsClaimModal open={showClaim} onClose={() => setShowClaim(false)} ctx={ctx} project={null} container={null} />
    </div>
  );
}

function WarehouseAllocationsSubTab({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [projectFilter, setProjectFilter] = useState('All');
  const [statusFilter, setStatusFilter] = useState('All');
  const [releaseFor, setReleaseFor] = useState(null);
  const [cancelFor, setCancelFor] = useState(null);
  const [reallocateFor, setReallocateFor] = useState(null);
  const [historyFor, setHistoryFor] = useState(null);
  const [convertFor, setConvertFor] = useState(null);
  const canDecideAdmin = ctx.currentRole === 'Admin';
  const canDecideLogistic = ctx.currentRole === 'Logistic Manager';
  const filtered = ctx.materialAllocations.filter(a =>
    (projectFilter === 'All' || a.projectId === projectFilter) && (statusFilter === 'All' || a.status === statusFilter)
  );
  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
        <div className="flex items-center gap-2 flex-wrap">
          <Select value={projectFilter} onChange={e => setProjectFilter(e.target.value)} className="!w-auto !py-1 !text-xs"><option value="All">All Projects</option>{ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select>
          <Select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="!w-auto !py-1 !text-xs"><option value="All">All Statuses</option>{ALLOCATION_STATUSES.map(s => <option key={s}>{s}</option>)}</Select>
        </div>
        {ctx.canAllocateMaterial && <Button onClick={() => setShowAdd(true)}>+ New Allocation</Button>}
      </div>
      {filtered.length === 0 ? <EmptyState text="No allocations match." /> : (
        <div className="space-y-2">
          {filtered.map(a => {
            const material = ctx.warehouseMaterials.find(m => m.id === a.materialId);
            const project = ctx.projects.find(p => p.id === a.projectId);
            const scope = project && project.scopes.find(s => s.id === a.scopeId);
            const active = ['Reserved', 'Confirmed', 'Partially Released'].includes(a.status);
            const allocatedRemaining = a.quantityAllocated - a.quantityReleased;
            const releasedAwaitingDelivery = a.quantityReleased - a.quantityDelivered;
            return (
              <div key={a.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap">
                  <div className="cursor-pointer" onClick={() => setHistoryFor(a)}>
                    <p className="text-sm font-bold hover:underline">{material ? material.name : '—'} <span className="font-normal text-[var(--leon-black)]/50">— {a.quantityAllocated} {a.unitOfMeasure} {project ? `→ ${project.name}` : ''}{scope ? ` / ${scope.name}` : ''}</span></p>
                    <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{[a.building, a.floor, a.unit].filter(Boolean).join(' · ') || '—'} · Allocated {fmtDate(a.allocationDate)} by {a.allocatedBy}{a.requiredDate ? ` · Required by ${fmtDate(a.requiredDate)}` : ''}</p>
                  </div>
                  <StatusBadge status={a.status} />
                </div>
                <div className="grid grid-cols-4 gap-2 mt-2 text-[11px]">
                  <div><p className="text-[var(--leon-black)]/40 uppercase">Allocated (unreleased)</p><p className="font-semibold">{allocatedRemaining} {a.unitOfMeasure}</p></div>
                  <div><p className="text-[var(--leon-black)]/40 uppercase">Released (awaiting delivery)</p><p className="font-semibold">{releasedAwaitingDelivery} {a.unitOfMeasure}</p></div>
                  <div><p className="text-[var(--leon-black)]/40 uppercase">Delivered</p><p className="font-semibold">{a.quantityDelivered} {a.unitOfMeasure}</p></div>
                  <div><p className="text-[var(--leon-black)]/40 uppercase">Total</p><p className="font-semibold">{a.quantityAllocated} {a.unitOfMeasure}</p></div>
                </div>
                {a.notes && <p className="text-xs text-[var(--leon-black)]/50 mt-1">{a.notes}</p>}
                {a.stockConversionRequest && a.status !== 'Converted to Stock' && (
                  <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
                    <p className="text-xs font-semibold text-[var(--leon-brown)]">Stock conversion requested — value {fmtMoney(a.stockConversionRequest.value)} by {a.stockConversionRequest.requestedBy}, {fmtDate(a.stockConversionRequest.requestedDate)}</p>
                    <div className="flex items-center gap-1.5 mt-1">
                      <Badge tone={a.stockConversionRequest.adminApproved ? 'green' : 'neutral'}>Admin {a.stockConversionRequest.adminApproved ? '✓' : 'pending'}</Badge>
                      <Badge tone={a.stockConversionRequest.logisticManagerApproved ? 'green' : 'neutral'}>Logistic Manager {a.stockConversionRequest.logisticManagerApproved ? '✓' : 'pending'}</Badge>
                      {canDecideAdmin && !a.stockConversionRequest.adminApproved && <Button size="sm" onClick={() => ctx.decideStockConversion(a.id, 'admin')}>Approve (Admin)</Button>}
                      {canDecideLogistic && !a.stockConversionRequest.logisticManagerApproved && <Button size="sm" onClick={() => ctx.decideStockConversion(a.id, 'logisticManager')}>Approve (Logistic Manager)</Button>}
                    </div>
                  </div>
                )}
                <div className="flex items-center gap-2 mt-2 flex-wrap">
                  <Button size="sm" variant="ghost" onClick={() => setHistoryFor(a)}>Details / History ({a.history.length})</Button>
                  {active && ctx.canAssistWarehouse && <Button size="sm" variant="ghost" onClick={() => setReleaseFor(a)}>Release</Button>}
                  {active && ctx.canControlInventory && <Button size="sm" variant="ghost" onClick={() => setReallocateFor(a)}>Reallocate</Button>}
                  {active && ctx.canAllocateMaterial && <Button size="sm" variant="ghost" onClick={() => setCancelFor(a)}>Cancel</Button>}
                  {active && !a.stockConversionRequest && project && project.pipelineStatus === 'Completed Job' && ctx.canApproveStockConversion && (
                    <Button size="sm" variant="outline" onClick={() => setConvertFor(a)}>Convert to Stock</Button>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      )}
      <AddAllocationModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} />
      <ReleaseAllocationModal open={!!releaseFor} allocation={releaseFor} onClose={() => setReleaseFor(null)} ctx={ctx} />
      <CancelAllocationModal open={!!cancelFor} allocation={cancelFor} onClose={() => setCancelFor(null)} ctx={ctx} />
      <ReallocateModal open={!!reallocateFor} allocation={reallocateFor} onClose={() => setReallocateFor(null)} ctx={ctx} />
      <AllocationDetailModal open={!!historyFor} allocation={historyFor} onClose={() => setHistoryFor(null)} ctx={ctx} />
      <RequestStockConversionModal open={!!convertFor} allocation={convertFor} onClose={() => setConvertFor(null)} ctx={ctx} />
    </div>
  );
}
// Completed-job leftover materials -> stock (Phase 9) — captures an
// editable value (not blindly the material's own unitCost) at request time;
// the actual reversal only happens once both Admin and Logistic Manager
// approve (see decideStockConversion).
function RequestStockConversionModal({ open, allocation, onClose, ctx }) {
  const [value, setValue] = useState('');
  const [notes, setNotes] = useState('');
  useEffect(() => {
    if (open && allocation) {
      const material = ctx.warehouseMaterials.find(m => m.id === allocation.materialId);
      const remaining = allocation.quantityAllocated - allocation.quantityReleased;
      setValue(String(material ? remaining * material.unitCost : 0));
      setNotes('');
    }
  }, [open, allocation]);
  if (!allocation) return null;
  const material = ctx.warehouseMaterials.find(m => m.id === allocation.materialId);
  const remaining = allocation.quantityAllocated - allocation.quantityReleased;
  function submit() { ctx.requestStockConversion(allocation.id, { value, notes }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Convert to Stock" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Request Conversion</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">{remaining} {allocation.unitOfMeasure} of {material ? material.name : 'this material'} becomes unassigned inventory once both Admin and Logistic Manager approve — reducing this project's material cost by the value below.</p>
        <Field label="Value" hint="Editable — leftover stock may not be worth full unit cost."><TextInput type="number" value={value} onChange={e => setValue(e.target.value)} /></Field>
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
function AllocationDetailModal({ open, allocation, onClose, ctx }) {
  if (!allocation) return null;
  const material = ctx.warehouseMaterials.find(m => m.id === allocation.materialId);
  const project = ctx.projects.find(p => p.id === allocation.projectId);
  const scope = project && project.scopes.find(s => s.id === allocation.scopeId);
  const relatedReleases = ctx.warehouseReleases.filter(r => r.lines.some(l => l.allocationId === allocation.id));
  const relatedPackingLists = ctx.packingLists.filter(pl => pl.lines.some(l => l.allocationId === allocation.id));
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Allocation — ${material ? material.name : allocation.materialId}`}
      fields={[
        { label: 'Project', value: project ? project.name : '—' }, { label: 'Scope', value: scope ? scope.name : '—' },
        { label: 'Location', value: [allocation.building, allocation.floor, allocation.unit].filter(Boolean).join(' · ') },
        { label: 'Status', value: allocation.status }, { label: 'Quantity Allocated', value: `${allocation.quantityAllocated} ${allocation.unitOfMeasure}` },
        { label: 'Quantity Released', value: `${allocation.quantityReleased} ${allocation.unitOfMeasure}` }, { label: 'Quantity Delivered', value: `${allocation.quantityDelivered} ${allocation.unitOfMeasure}` },
        { label: 'Allocated By', value: allocation.allocatedBy }, { label: 'Allocation Date', value: fmtDate(allocation.allocationDate) },
        { label: 'Required Date', value: allocation.requiredDate ? fmtDate(allocation.requiredDate) : '—' },
        ...(ctx.canSeeFin ? [{ label: 'Project Cost Contribution', value: fmtMoney(allocation.costContribution) }] : []),
        { label: 'Notes', value: allocation.notes },
      ]}
      history={allocation.history}
    >
      {(relatedReleases.length > 0 || relatedPackingLists.length > 0) && (
        <div>
          <p className="text-xs font-semibold mb-1">Related Activity</p>
          {relatedReleases.map(r => <p key={r.id} className="text-xs text-[var(--leon-black)]/60">Release {r.releaseNumber} — {fmtDate(r.releaseDate)} by {r.releasedBy} ({r.status})</p>)}
          {relatedPackingLists.map(pl => <p key={pl.id} className="text-xs text-[var(--leon-black)]/60">Packing List {pl.packingListNumber}{pl.deliveryId ? ' — delivery record created' : ''}</p>)}
        </div>
      )}
    </RecordDetailModal>
  );
}
function AddAllocationModal({ open, onClose, ctx }) {
  const blank = { materialId: '', projectId: '', scopeId: '', building: '', floor: '', unit: '', quantityAllocated: '', requiredDate: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [error, setError] = useState('');
  useEffect(() => { if (open) { setForm({ ...blank, materialId: ctx.warehouseMaterials[0]?.id || '', projectId: ctx.projects[0]?.id || '' }); setError(''); } }, [open]);
  const material = ctx.warehouseMaterials.find(m => m.id === form.materialId);
  const project = ctx.projects.find(p => p.id === form.projectId);
  const available = material ? availableQuantity(material, ctx.materialAllocations) : 0;
  function submit() {
    if (!material || !project) return;
    const result = ctx.addMaterialAllocation({ ...form, quantityAllocated: Number(form.quantityAllocated), unitOfMeasure: material.unitOfMeasure, warehouseId: material.warehouseId });
    if (result && result.error) { setError(result.error); return; }
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="New Material Allocation" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Reserve</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Material">
            <Select value={form.materialId} onChange={e => setForm({ ...form, materialId: e.target.value })}>
              {ctx.warehouseMaterials.map(m => <option key={m.id} value={m.id}>{m.name} ({m.category})</option>)}
            </Select>
          </Field>
          <Field label="Available Quantity"><p className="text-sm font-bold pt-2">{available} {material ? material.unitOfMeasure : ''}</p></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Project">
            <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>
              {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <Field label="Scope (optional)">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
              <option value="">— none —</option>
              {(project ? project.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Building"><TextInput value={form.building} onChange={e => setForm({ ...form, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Unit / Area"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label={`Quantity to Allocate${material ? ` (${material.unitOfMeasure})` : ''}`}><TextInput type="number" value={form.quantityAllocated} onChange={e => setForm({ ...form, quantityAllocated: e.target.value })} /></Field>
          <Field label="Required Date"><TextInput type="date" value={form.requiredDate} onChange={e => setForm({ ...form, requiredDate: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
      </div>
    </Modal>
  );
}
function ReleaseAllocationModal({ open, allocation, onClose, ctx }) {
  const [qty, setQty] = useState('');
  const [notes, setNotes] = useState('');
  useEffect(() => { if (open && allocation) { setQty(String(allocation.quantityAllocated - allocation.quantityReleased)); setNotes(''); } }, [open, allocation]);
  if (!allocation) return null;
  const material = ctx.warehouseMaterials.find(m => m.id === allocation.materialId);
  function submit() {
    if (!qty) return;
    ctx.releaseAllocation(allocation.id, Number(qty), notes);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Release Material" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Release</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Remaining to release: <strong>{allocation.quantityAllocated - allocation.quantityReleased} {allocation.unitOfMeasure}</strong> of {material ? material.name : '—'}</p>
        <Field label="Quantity to Release Now"><TextInput type="number" value={qty} onChange={e => setQty(e.target.value)} /></Field>
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
function CancelAllocationModal({ open, allocation, onClose, ctx }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  if (!allocation) return null;
  function submit() {
    ctx.cancelAllocation(allocation.id, reason);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Cancel Allocation" footer={<><Button variant="ghost" onClick={onClose}>Back</Button><Button variant="danger" onClick={submit}>Cancel Allocation</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">This frees the reserved quantity back to available stock. Recorded in the allocation's audit history.</p>
        <Field label="Reason"><TextArea rows={2} value={reason} onChange={e => setReason(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
function ReallocateModal({ open, allocation, onClose, ctx }) {
  const blank = { projectId: '', scopeId: '', building: '', floor: '', unit: '', reason: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open, allocation]);
  if (!allocation) return null;
  const project = ctx.projects.find(p => p.id === form.projectId);
  function submit() {
    if (!form.projectId) return;
    ctx.reallocateMaterial(allocation.id, form.projectId, form.scopeId || null, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Reallocate to a Different Project" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Reallocate</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Moves the remaining {allocation.quantityAllocated - allocation.quantityReleased} {allocation.unitOfMeasure} to a new project. The original allocation is marked Reallocated and kept in full — never overwritten.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="New Project">
            <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>
              <option value="">— select —</option>
              {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <Field label="Scope (optional)">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
              <option value="">— none —</option>
              {(project ? project.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Building"><TextInput value={form.building} onChange={e => setForm({ ...form, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Unit / Area"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
        </div>
        <Field label="Reason"><TextArea rows={2} value={form.reason} onChange={e => setForm({ ...form, reason: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Warehouse Release → Packing List → Delivery (§5-7) — Release formally
// pulls allocated material out for a shipment (reuses the existing Release
// stock-withdrawal logic under the hood); a Packing List is generated from a
// Release without re-entry, supporting multiple partial packing lists per
// release; "Create Delivery Record" hands off straight into the project's
// existing Delivery tab. ----
function packedQtyForLine(packingLists, releaseId, allocationId) {
  return packingLists.filter(pl => pl.releaseId === releaseId).reduce((sum, pl) => sum + pl.lines.filter(l => l.allocationId === allocationId).reduce((s, l) => s + l.quantity, 0), 0);
}
function WarehouseReleasesSubTab({ ctx }) {
  const [projectFilter, setProjectFilter] = useState('All');
  const [newReleaseOpen, setNewReleaseOpen] = useState(false);
  const [packFor, setPackFor] = useState(null);
  const [printPL, setPrintPL] = useState(null);
  const releases = ctx.warehouseReleases.filter(r => projectFilter === 'All' || r.projectId === projectFilter).sort((a, b) => (a.releaseDate < b.releaseDate ? 1 : -1));
  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
        <Select value={projectFilter} onChange={e => setProjectFilter(e.target.value)} className="!w-auto !py-1 !text-xs"><option value="All">All Projects</option>{ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select>
        {ctx.canAssistWarehouse && <Button onClick={() => setNewReleaseOpen(true)}>+ New Release</Button>}
      </div>
      {releases.length === 0 ? <EmptyState text="No releases yet. Release allocated material from the Allocations tab, or start a new Release here." /> : (
        <div className="space-y-3">
          {releases.map(r => {
            const project = ctx.projects.find(p => p.id === r.projectId);
            const scope = project && project.scopes.find(s => s.id === r.scopeId);
            const pls = ctx.packingLists.filter(pl => pl.releaseId === r.id);
            const hasUnpacked = r.lines.some(l => packedQtyForLine(ctx.packingLists, r.id, l.allocationId) < l.quantity);
            return (
              <div key={r.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap">
                  <div className="flex items-center gap-2 flex-wrap"><Badge tone="black">{r.releaseNumber}</Badge>{project && <Badge tone="neutral">{project.name}</Badge>}{scope && <Badge tone="neutral">{scope.name}</Badge>}<StatusBadge status={r.status} /></div>
                  {hasUnpacked && ctx.canAssistWarehouse && <Button size="sm" variant="outline" onClick={() => setPackFor(r)}>Generate Packing List</Button>}
                </div>
                <p className="text-xs text-[var(--leon-black)]/50 mt-1">Released {fmtDate(r.releaseDate)} by {r.releasedBy}{r.notes ? ` · ${r.notes}` : ''}</p>
                <ul className="text-xs mt-1.5 space-y-0.5">
                  {r.lines.map((l, i) => { const m = ctx.warehouseMaterials.find(x => x.id === l.materialId); return <li key={i}>{l.quantity} {m ? m.unitOfMeasure : ''} — {m ? m.name : l.materialId}</li>; })}
                </ul>
                {pls.length > 0 && (
                  <div className="mt-2 pt-2 border-t border-[var(--leon-line)] space-y-1.5">
                    {pls.map(pl => (
                      <div key={pl.id} className="flex items-center justify-between gap-2 flex-wrap text-xs">
                        <span><Badge tone="neutral">{pl.packingListNumber}</Badge> {pl.lines.length} line(s){pl.deliveryId ? ' · delivery record created' : ''}</span>
                        <div className="flex items-center gap-1">
                          <Button size="sm" variant="ghost" onClick={() => setPrintPL(pl)}>View / Print</Button>
                          {!pl.deliveryId && ctx.canAssistWarehouse && <Button size="sm" variant="ghost" onClick={async () => { const res = await ctx.createDeliveryFromPackingList(pl.id); if (res && res.ok) ctx.goProjectTab(pl.projectId, 'delivery'); }}>Create Delivery Record</Button>}
                          {pl.deliveryId && <Button size="sm" variant="ghost" onClick={() => ctx.goProjectTab(pl.projectId, 'delivery')}>Open Delivery →</Button>}
                        </div>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
      <CreateReleaseModal open={newReleaseOpen} onClose={() => setNewReleaseOpen(false)} ctx={ctx} />
      <GeneratePackingListModal open={!!packFor} release={packFor} onClose={() => setPackFor(null)} ctx={ctx} />
      <PackingListPrintModal open={!!printPL} packingList={printPL} onClose={() => setPrintPL(null)} ctx={ctx} />
    </div>
  );
}
function CreateReleaseModal({ open, onClose, ctx }) {
  const [projectId, setProjectId] = useState('');
  const [scopeId, setScopeId] = useState('');
  const [notes, setNotes] = useState('');
  const [qtys, setQtys] = useState({});
  const [error, setError] = useState('');
  useEffect(() => { if (open) { setProjectId(''); setScopeId(''); setNotes(''); setQtys({}); setError(''); } }, [open]);
  const project = ctx.projects.find(p => p.id === projectId);
  const releasable = ctx.materialAllocations.filter(a => a.projectId === projectId && ['Reserved', 'Confirmed', 'Partially Released'].includes(a.status) && (!scopeId || a.scopeId === scopeId));
  async function submit() {
    const lines = releasable.map(a => ({ allocationId: a.id, quantity: Number(qtys[a.id]) || 0 })).filter(l => l.quantity > 0);
    const result = await ctx.createWarehouseRelease(projectId, scopeId || null, lines, notes);
    if (result && result.error) { setError(result.error); return; }
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="New Release for Delivery" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create Release</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Project"><Select value={projectId} onChange={e => { setProjectId(e.target.value); setScopeId(''); }}><option value="">— select —</option>{ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
          <Field label="Scope (optional)"><Select value={scopeId} onChange={e => setScopeId(e.target.value)}><option value="">— all scopes —</option>{(project ? project.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
        {projectId && (
          releasable.length === 0 ? <EmptyState text="No releasable allocations for this project/scope." /> : (
            <div className="border border-[var(--leon-line)] rounded-lg divide-y divide-[var(--leon-line)]">
              {releasable.map(a => {
                const m = ctx.warehouseMaterials.find(x => x.id === a.materialId);
                const remaining = a.quantityAllocated - a.quantityReleased;
                return (
                  <div key={a.id} className="p-2 flex items-center justify-between gap-2 text-xs">
                    <span>{m ? m.name : a.materialId} — allocated {remaining} {a.unitOfMeasure}</span>
                    <TextInput type="number" className="!w-24 !py-1 !text-xs" value={qtys[a.id] || ''} onChange={e => setQtys({ ...qtys, [a.id]: e.target.value })} placeholder="Qty" />
                  </div>
                );
              })}
            </div>
          )
        )}
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
      </div>
    </Modal>
  );
}
function GeneratePackingListModal({ open, release, onClose, ctx }) {
  const [address, setAddress] = useState('');
  const [deliveryContact, setDeliveryContact] = useState('');
  const [plannedDeliveryDate, setPlannedDeliveryDate] = useState('');
  const [notes, setNotes] = useState('');
  const [qtys, setQtys] = useState({});
  const [error, setError] = useState('');
  useEffect(() => {
    if (open && release) {
      const project = ctx.projects.find(p => p.id === release.projectId);
      setAddress((project && project.address) || ''); setDeliveryContact(''); setPlannedDeliveryDate(''); setNotes(''); setError('');
      const initial = {};
      release.lines.forEach(l => { initial[l.allocationId] = String(l.quantity - packedQtyForLine(ctx.packingLists, release.id, l.allocationId)); });
      setQtys(initial);
    }
  }, [open, release]);
  if (!release) return null;
  async function submit() {
    const selected = release.lines.map(l => ({ allocationId: l.allocationId, materialId: l.materialId, quantity: Number(qtys[l.allocationId]) || 0 })).filter(l => l.quantity > 0);
    const result = await ctx.generatePackingList(release.id, selected, { address, deliveryContact, plannedDeliveryDate, notes });
    if (result && result.error) { setError(result.error); return; }
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Generate Packing List — from ${release.releaseNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Generate Packing List</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Select which released lines and quantities go into this shipment — supports multiple partial packing lists per release.</p>
        <div className="border border-[var(--leon-line)] rounded-lg divide-y divide-[var(--leon-line)]">
          {release.lines.map(l => {
            const m = ctx.warehouseMaterials.find(x => x.id === l.materialId);
            const remaining = l.quantity - packedQtyForLine(ctx.packingLists, release.id, l.allocationId);
            return (
              <div key={l.allocationId} className="p-2 flex items-center justify-between gap-2 text-xs">
                <span>{m ? m.name : l.materialId} — released {l.quantity}, remaining to pack {remaining} {m ? m.unitOfMeasure : ''}</span>
                <TextInput type="number" className="!w-24 !py-1 !text-xs" value={qtys[l.allocationId] || ''} onChange={e => setQtys({ ...qtys, [l.allocationId]: e.target.value })} placeholder="Qty" />
              </div>
            );
          })}
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Delivery Address"><TextInput value={address} onChange={e => setAddress(e.target.value)} /></Field>
          <Field label="Delivery Contact"><TextInput value={deliveryContact} onChange={e => setDeliveryContact(e.target.value)} /></Field>
        </div>
        <Field label="Planned Delivery Date"><TextInput type="date" value={plannedDeliveryDate} onChange={e => setPlannedDeliveryDate(e.target.value)} /></Field>
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
      </div>
    </Modal>
  );
}
function PackingListPrintModal({ open, packingList, onClose, ctx }) {
  if (!packingList) return null;
  const project = ctx.projects.find(p => p.id === packingList.projectId);
  const body = (
    <>
      <div>
        <p className="text-lg font-bold">Packing List {packingList.packingListNumber}</p>
        <p className="text-xs text-[var(--leon-black)]/60">{project ? project.name : packingList.projectName} · {packingList.address}</p>
        <p className="text-xs text-[var(--leon-black)]/60">Delivery Contact: {packingList.deliveryContact || '—'} · Planned Delivery: {packingList.plannedDeliveryDate ? fmtDate(packingList.plannedDeliveryDate) : '—'}</p>
        <p className="text-xs text-[var(--leon-black)]/60">Prepared by {packingList.preparedBy} on {fmtDate(packingList.preparedDate)}</p>
      </div>
      <table className="w-full text-xs">
        <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase border-b border-[var(--leon-line)]"><th className="py-1 pr-2">Item</th><th className="py-1 pr-2">SKU</th><th className="py-1 pr-2">Scope</th><th className="py-1 pr-2">Qty</th><th className="py-1 pr-2">Package</th></tr></thead>
        <tbody>
          {packingList.lines.map((l, i) => (
            <tr key={i} className="border-b border-[var(--leon-line)]"><td className="py-1 pr-2">{l.itemName}</td><td className="py-1 pr-2">{l.sku || '—'}</td><td className="py-1 pr-2">{l.scopeName || '—'}</td><td className="py-1 pr-2">{l.quantity} {l.unitOfMeasure}</td><td className="py-1 pr-2">{l.packageInfo || '—'}</td></tr>
          ))}
        </tbody>
      </table>
      {packingList.notes && <p className="text-xs text-[var(--leon-black)]/60">{packingList.notes}</p>}
    </>
  );
  return (
    <>
      <Modal open={open} onClose={onClose} wide title={`Packing List ${packingList.packingListNumber}`} footer={<><PrintButton onClick={() => window.print()} label="Print / PDF" /><Button variant="ghost" onClick={onClose}>Close</Button></>}>
        <div className="space-y-3">{body}</div>
      </Modal>
      {/* On-screen preview above lives inside Modal's own .no-print wrapper,
          so the printed output has to be a sibling .print-only block instead
          — an ancestor display:none hides descendants regardless of their
          own class, so a nested .print-area alone (no .print-only) never
          actually printed anything. */}
      {open && <div className="print-only print-area p-8">{body}</div>}
    </>
  );
}

function ImportValidationSubTab({ ctx }) {
  const v = IMPORT_VALIDATION_SUMMARY;
  return (
    <div className="space-y-4">
      <p className="text-xs text-[var(--leon-black)]/50">Source: <strong>{v.source}</strong> · Board: {v.board} · Imported {fmtDate(v.importDate)}</p>
      <div className="grid sm:grid-cols-3 gap-3">
        <StatBox label="Total Materials Imported" value={String(v.totalMaterialsImported)} />
        <StatBox label="Total Transactions Imported" value={String(v.totalTransactionsImported)} />
        <StatBox label="Total Rows Imported" value={String(v.totalRowsImported)} />
      </div>
      <Collapsible id="import-val-dupes" title="Duplicate Records Found" count={v.duplicateRecordsFound.materialNameDuplicates}>
        <p className="text-xs text-[var(--leon-black)]/60">{v.duplicateRecordsFound.note}</p>
      </Collapsible>
      <Collapsible id="import-val-missing-ref" title="Materials Missing Reference Number" count={v.missingRequiredFields.materialsMissingReferenceNumber.length}>
        {v.missingRequiredFields.materialsMissingReferenceNumber.length === 0 ? <EmptyState text="None." /> : (
          <ul className="text-xs space-y-0.5">{v.missingRequiredFields.materialsMissingReferenceNumber.map((r, i) => <li key={i}>{r.material} <span className="text-[var(--leon-black)]/40">(source row {r.row})</span></li>)}</ul>
        )}
      </Collapsible>
      <Collapsible id="import-val-missing-initial" title="Materials Missing Initial Stock" count={v.missingRequiredFields.materialsMissingInitialStock.length}>
        {v.missingRequiredFields.materialsMissingInitialStock.length === 0 ? <EmptyState text="None." /> : (
          <ul className="text-xs space-y-0.5">{v.missingRequiredFields.materialsMissingInitialStock.map((r, i) => <li key={i}>{r.material} <span className="text-[var(--leon-black)]/40">(source row {r.row})</span></li>)}</ul>
        )}
      </Collapsible>
      <Collapsible id="import-val-missing-qty" title="Transactions Missing Quantity" count={v.missingRequiredFields.transactionsMissingQuantity.length}>
        {v.missingRequiredFields.transactionsMissingQuantity.length === 0 ? <EmptyState text="None." /> : (
          <ul className="text-xs space-y-0.5">{v.missingRequiredFields.transactionsMissingQuantity.map((r, i) => <li key={i}>{r.material} <span className="text-[var(--leon-black)]/40">(source row {r.row})</span></li>)}</ul>
        )}
      </Collapsible>
      <Collapsible id="import-val-review" title="Data Requiring Manual Review" count={v.dataRequiringManualReview.length}>
        {v.dataRequiringManualReview.length === 0 ? <EmptyState text="None." /> : (
          <ul className="text-xs space-y-0.5">{v.dataRequiringManualReview.map((r, i) => <li key={i}>{r.material} <span className="text-[var(--leon-black)]/40">(source row {r.row} — raw Type={String(r.raw_type)}, raw Status={String(r.raw_status)})</span></li>)}</ul>
        )}
      </Collapsible>
      <Collapsible id="import-val-unmapped" title="Records That Could Not Be Mapped" count={v.recordsCouldNotBeMapped.length}>
        <EmptyState text="None — every row was mapped to a material or transaction." />
      </Collapsible>
    </div>
  );
}

// ============================================================================
// Vendors — shared directory with contact info, default payment terms, and
// every estimate placed with that vendor across all projects.
// ============================================================================
const DIRECTORY_SORT_OPTIONS = [
  { key: 'name-asc', label: 'Name (A–Z)' },
  { key: 'name-desc', label: 'Name (Z–A)' },
  { key: 'date-desc', label: 'Newest First' },
  { key: 'date-asc', label: 'Oldest First' },
];
function directorySortComparators(nameOf) {
  return {
    'name-asc': (a, b) => textAsc(nameOf(a), nameOf(b)),
    'name-desc': (a, b) => textDesc(nameOf(a), nameOf(b)),
    'date-desc': (a, b) => dateDesc(a.createdDate, b.createdDate),
    'date-asc': (a, b) => dateAsc(a.createdDate, b.createdDate),
  };
}
// Vendors / Freight Forwarders / Subcontractors are three directories behind one
// screen. Icons follow the rest of the app: 🏭 is the Vendors nav entry, 🚢 is
// export and shipping everywhere else, and 🔧 is already Subcontractors on the
// About Us page.
const VENDOR_DIRECTORY_TABS = [
  { key: 'vendor', label: 'Vendors', icon: '🏭' },
  { key: 'forwarder', label: 'Freight Forwarders', icon: '🚢' },
  { key: 'subcontractor', label: 'Subcontractors', icon: '🔧' },
];
function VendorsView({ ctx }) {
  const [tab, setTab] = useHubSection(ctx, 'vendors', 'vendor'); // 'vendor' | 'forwarder' | 'subcontractor'
  const [showNew, setShowNew] = useState(false);
  const [search, setSearch] = useState('');
  const [sortKey, setSortKey] = useState('name-asc');
  const list = tab === 'vendor' ? ctx.vendors : tab === 'forwarder' ? ctx.freightForwarders : ctx.subcontractors;
  const estimateKey = tab === 'vendor' ? 'vendorEstimates' : 'freightEstimates';
  const idKey = tab === 'vendor' ? 'vendorId' : 'forwarderId';
  const allInvoices = useMemo(() => allApInvoices(ctx.projects), [ctx.projects]);
  const nameOf = x => x.companyName || x.name || '';
  const q = search.trim().toLowerCase();
  const filtered = list.filter(x => {
    if (!q) return true;
    return [nameOf(x), x.contactPerson, x.contactName, x.email].some(v => (v || '').toLowerCase().includes(q));
  });
  const sorted = sortList(filtered, sortKey, directorySortComparators(nameOf));

  return (
    <div>
      <div className="flex items-center justify-between mb-5">
        <h1 className="text-2xl font-bold">{tab === 'vendor' ? 'Vendors' : tab === 'forwarder' ? 'Freight Forwarders' : 'Subcontractors'}</h1>
        <Button onClick={() => setShowNew(true)}>{tab === 'vendor' ? '+ New Vendor' : tab === 'forwarder' ? '+ New Freight Forwarder' : '+ New Subcontractor'}</Button>
      </div>
      {/* The shared Tabs component rather than three hand-rolled buttons: it
          already carries the icons, the wrapping and the no-print treatment,
          and this bar was a copy of its styling that had drifted without any. */}
      <div className="mb-5"><Tabs tabs={VENDOR_DIRECTORY_TABS} active={tab} onChange={setTab} /></div>
      <div className="flex flex-wrap items-center gap-2 mb-4">
        <TextInput value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by name, contact, or email…" className="!w-64" />
        <SortSelect value={sortKey} onChange={setSortKey} options={DIRECTORY_SORT_OPTIONS} />
        <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{sorted.length} of {list.length}</span>
      </div>
      <div className="space-y-3">
        {tab === 'subcontractor' ? sorted.map(s => {
          const summary = partyFinancialSummary(allInvoices.filter(i => i.vendorId === s.id));
          return (
            <div key={s.id} onClick={() => ctx.goSubcontractorDetail(s.id)} className="hub-card cursor-pointer bg-white border border-[var(--leon-line)] rounded-xl p-4">
              <div className="flex flex-wrap items-start justify-between gap-3">
                <div>
                  <h3 className="font-bold">{s.companyName} <span className="font-normal text-[var(--leon-black)]/50">— {s.contactName}</span></h3>
                  <p className="text-xs text-[var(--leon-black)]/50">{s.trade} · {s.phone} · {s.email}</p>
                </div>
                <div className="flex items-center gap-2 shrink-0">
                  {s.status !== 'Active' && <Badge tone="neutral">{s.status}</Badge>}
                  {s.registrationStatus !== 'Approved' && <StatusBadge status={s.registrationStatus} />}
                  {summary.totalOpenBalance > 0 && <Badge tone={summary.pastDueCount > 0 ? 'red' : 'yellow'}>{fmtMoney(summary.totalOpenBalance)} open</Badge>}
                </div>
              </div>
            </div>
          );
        }) : sorted.map(v => {
          const estimates = [];
          ctx.projects.forEach(p => { (p[estimateKey] || []).filter(e => e[idKey] === v.id).forEach(e => estimates.push(e)); });
          const rate = vendorAcceptanceRate(estimates);
          const summary = partyFinancialSummary(allInvoices.filter(i => i.vendorId === v.id));
          return (
            <div key={v.id} onClick={() => ctx.goVendorDetail(v.id, tab)} className="hub-card cursor-pointer bg-white border border-[var(--leon-line)] rounded-xl p-4">
              <div className="flex flex-wrap items-start justify-between gap-3">
                <div>
                  <h3 className="font-bold">{v.name}</h3>
                  <p className="text-xs text-[var(--leon-black)]/50">{v.vendorType} · {v.contactPerson} · {v.phone} · {v.email}</p>
                </div>
                <div className="flex items-center gap-2 shrink-0">
                  {v.status !== 'Active' && <Badge tone="neutral">{v.status}</Badge>}
                  {rate.pct !== null && <Badge tone={rate.pct >= 50 ? 'green' : 'yellow'}>{fmtPct(rate.pct)} PO rate</Badge>}
                  {summary.totalOpenBalance > 0 && <Badge tone={summary.pastDueCount > 0 ? 'red' : 'yellow'}>{fmtMoney(summary.totalOpenBalance)} open</Badge>}
                </div>
              </div>
            </div>
          );
        })}
        {sorted.length === 0 && <EmptyState text={list.length === 0 ? (tab === 'vendor' ? 'No vendors yet.' : tab === 'forwarder' ? 'No freight forwarders yet.' : 'No subcontractors yet.') : 'No matches.'} />}
      </div>
      {tab === 'subcontractor' ? <AddSubcontractorModal open={showNew} onClose={() => setShowNew(false)} ctx={ctx} /> : <AddVendorModal open={showNew} onClose={() => setShowNew(false)} ctx={ctx} type={tab} />}
    </div>
  );
}
function VendorBillingFields({ billing, onChange }) {
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3 space-y-3">
      <div className="flex items-center gap-4">
        <label className="flex items-center gap-1.5 text-sm"><input type="radio" checked={billing.sameAsCompany} onChange={() => onChange({ sameAsCompany: true })} /> Same as company address</label>
        <label className="flex items-center gap-1.5 text-sm"><input type="radio" checked={!billing.sameAsCompany} onChange={() => onChange({ sameAsCompany: false })} /> Use different billing address</label>
      </div>
      {!billing.sameAsCompany && (
        <>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Billing Company Name"><TextInput value={billing.companyName} onChange={e => onChange({ companyName: e.target.value })} /></Field>
            <Field label="Billing Contact"><TextInput value={billing.contact} onChange={e => onChange({ contact: e.target.value })} /></Field>
          </div>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Billing Email"><TextInput value={billing.email} onChange={e => onChange({ email: e.target.value })} /></Field>
            <Field label="Billing Phone"><TextInput value={billing.phone} onChange={e => onChange({ phone: e.target.value })} /></Field>
          </div>
          <Field label="Billing Address"><TextInput value={billing.address} onChange={e => onChange({ address: e.target.value })} /></Field>
          <div className="grid grid-cols-3 gap-3">
            <Field label="City"><TextInput value={billing.city} onChange={e => onChange({ city: e.target.value })} /></Field>
            <Field label="State / Province"><TextInput value={billing.state} onChange={e => onChange({ state: e.target.value })} /></Field>
            <Field label="ZIP / Postal Code"><TextInput value={billing.zip} onChange={e => onChange({ zip: e.target.value })} /></Field>
          </div>
          <Field label="Country"><TextInput value={billing.country} onChange={e => onChange({ country: e.target.value })} /></Field>
        </>
      )}
      <div className="grid grid-cols-3 gap-3">
        <Field label="Payment Terms" hint="e.g. Net 30"><TextInput value={billing.paymentTerms} onChange={e => onChange({ paymentTerms: e.target.value })} /></Field>
        <Field label="Preferred Payment Method"><Select value={billing.paymentMethod} onChange={e => onChange({ paymentMethod: e.target.value })}>{PAYMENT_METHODS.map(m => <option key={m}>{m}</option>)}</Select></Field>
        <Field label="Currency"><Select value={billing.currency} onChange={e => onChange({ currency: e.target.value })}>{CURRENCIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
      </div>
      <Field label="Tax / VAT Information"><TextInput value={billing.taxInfo} onChange={e => onChange({ taxInfo: e.target.value })} placeholder="e.g. W-9 on file, EIN, VAT #" /></Field>
      <Field label="Accounting Notes"><TextArea rows={2} value={billing.accountingNotes} onChange={e => onChange({ accountingNotes: e.target.value })} /></Field>
    </div>
  );
}
function AddVendorModal({ open, onClose, ctx, type }) {
  const blank = { name: '', vendorType: type === 'forwarder' ? 'Freight / Logistics' : VENDOR_TYPES[0], status: 'Active', contactPerson: '', contactTitle: '', phone: '', mobile: '', email: '', website: '', address: '', city: '', state: '', zip: '', country: '', depositPct: 50, balancePct: 50, notes: '', billing: makeBillingInfo() };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.name.trim()) return;
    const data = { ...form, defaultPaymentTerms: [{ label: 'Deposit', pct: Number(form.depositPct) }, { label: 'Balance on Delivery', pct: Number(form.balancePct) }] };
    if (type === 'forwarder') ctx.addFreightForwarder(data); else ctx.addVendor(data);
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title={type === 'forwarder' ? 'New Freight Forwarder' : 'New Vendor'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">General Information</p>
        <Field label="Vendor / Company Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          {type !== 'forwarder' && <Field label="Vendor Type"><Select value={form.vendorType} onChange={e => setForm({ ...form, vendorType: e.target.value })}>{VENDOR_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>}
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{VENDOR_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Main Contact Name"><TextInput value={form.contactPerson} onChange={e => setForm({ ...form, contactPerson: e.target.value })} /></Field>
          <Field label="Main Contact Title"><TextInput value={form.contactTitle} onChange={e => setForm({ ...form, contactTitle: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
          <Field label="Mobile"><TextInput value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
          <Field label="Website"><TextInput value={form.website} onChange={e => setForm({ ...form, website: e.target.value })} /></Field>
        </div>
        <Field label="Address"><TextInput value={form.address} onChange={e => setForm({ ...form, address: e.target.value })} /></Field>
        <div className="grid grid-cols-4 gap-3">
          <Field label="City"><TextInput value={form.city} onChange={e => setForm({ ...form, city: e.target.value })} /></Field>
          <Field label="State / Province"><TextInput value={form.state} onChange={e => setForm({ ...form, state: e.target.value })} /></Field>
          <Field label="ZIP / Postal Code"><TextInput value={form.zip} onChange={e => setForm({ ...form, zip: e.target.value })} /></Field>
          <Field label="Country"><TextInput value={form.country} onChange={e => setForm({ ...form, country: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Deposit %"><TextInput type="number" value={form.depositPct} onChange={e => setForm({ ...form, depositPct: e.target.value })} /></Field>
          <Field label="Balance %"><TextInput type="number" value={form.balancePct} onChange={e => setForm({ ...form, balancePct: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>

        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 pt-2">Billing Information</p>
        <VendorBillingFields billing={form.billing} onChange={fields => setForm({ ...form, billing: { ...form.billing, ...fields } })} />
      </div>
    </Modal>
  );
}
function makeVendorContactBlank() { return { role: VENDOR_CONTACT_ROLES[0], name: '', title: '', phone: '', mobile: '', email: '', preferredContactMethod: '', notes: '' }; }
function VendorContactsBlock({ ctx, vendor, isForwarder }) {
  const [adding, setAdding] = useState(false);
  const [form, setForm] = useState(makeVendorContactBlank());
  const addFn = isForwarder ? ctx.addFreightForwarderContact : ctx.addVendorContact;
  function submit() {
    if (!form.name.trim()) return;
    addFn(vendor.id, form);
    setForm(makeVendorContactBlank());
    setAdding(false);
  }
  return (
    <Collapsible title="Additional Contacts" count={vendor.contacts.length} right={<Button size="sm" variant="ghost" onClick={() => setAdding(a => !a)}>+ Add Contact</Button>}>
      {adding && (
        <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-3 space-y-2">
          <div className="grid grid-cols-2 gap-2">
            <Select value={form.role} onChange={e => setForm({ ...form, role: e.target.value })}>{VENDOR_CONTACT_ROLES.map(r => <option key={r}>{r}</option>)}</Select>
            <TextInput placeholder="Name" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} />
            <TextInput placeholder="Email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
          </div>
          <div className="grid grid-cols-3 gap-2">
            <TextInput placeholder="Phone" value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} />
            <TextInput placeholder="Mobile" value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} />
            <Select value={form.preferredContactMethod} onChange={e => setForm({ ...form, preferredContactMethod: e.target.value })}>
              <option value="">Preferred Method — none set</option>
              {PREFERRED_CONTACT_METHODS.map(m => <option key={m}>{m}</option>)}
            </Select>
          </div>
          <TextArea placeholder="Notes" rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} />
          <div className="flex justify-end gap-2"><Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button><Button size="sm" onClick={submit}>Save Contact</Button></div>
        </div>
      )}
      {vendor.contacts.length === 0 ? <EmptyState text="No additional contacts." /> : (
        <div className="space-y-1.5">
          {vendor.contacts.map(c => (
            <div key={c.id} className="border border-[var(--leon-line)] rounded-md px-3 py-2 text-xs">
              <div className="flex items-center justify-between gap-2">
                <div><Badge tone="neutral">{c.role}</Badge> <span className="font-semibold ml-1">{c.name}</span>{c.title ? ` — ${c.title}` : ''}</div>
                <div className="text-[var(--leon-black)]/50">{c.phone}{c.mobile ? ` / ${c.mobile}` : ''} {c.email && `· ${c.email}`}</div>
              </div>
              {(c.preferredContactMethod || c.notes) && (
                <p className="text-[var(--leon-black)]/40 mt-1">{c.preferredContactMethod && `Prefers: ${c.preferredContactMethod}`}{c.preferredContactMethod && c.notes ? ' · ' : ''}{c.notes}</p>
              )}
            </div>
          ))}
        </div>
      )}
    </Collapsible>
  );
}
// Open to everyone to add — no canEdit(...) gate — but once added, an entry
// can only be removed by an Admin (enforced in the ctx function itself, not
// just by hiding the ✕ button here).
function VendorCatalogSection({ ctx, vendor, isForwarder }) {
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const addFn = isForwarder ? ctx.addFreightForwarderCatalogEntry : ctx.addVendorCatalogEntry;
  const removeFn = isForwarder ? ctx.removeFreightForwarderCatalogEntry : ctx.removeVendorCatalogEntry;
  const isAdmin = ctx.currentRole === 'Admin';
  return (
    <Collapsible title="Catalog" count={vendor.catalog.length} right={<Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add Catalog Entry</Button>}>
      {vendor.catalog.length === 0 ? <EmptyState text="No catalog entries yet." /> : (
        <div className="space-y-1.5">
          {vendor.catalog.map(c => (
            <div key={c.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
              <div className="min-w-0 cursor-pointer" onClick={() => setDetailFor(c)}>
                <p className="text-sm font-semibold hover:underline">{c.name}{c.revisionNumber ? ` — Rev ${c.revisionNumber}` : ''}</p>
                <p className="text-xs text-[var(--leon-black)]/50">{fmtDate(c.date)}{c.scopeApplicable ? ` · ${c.scopeApplicable}` : ''} · Added by {c.addedBy}</p>
              </div>
              <div className="flex items-center gap-2 shrink-0">
                <AttachmentLink name={c.file} url={c.fileUrl} />
                {isAdmin && <IconBtn title="Remove (Admin only)" onClick={() => removeFn(vendor.id, c.id)}>✕</IconBtn>}
              </div>
            </div>
          ))}
        </div>
      )}
      <AddCatalogEntryModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} onSubmit={data => addFn(vendor.id, data)} />
      <RecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} title={`Catalog — ${detailFor?.name || ''}`} printable
        fields={detailFor ? [
          { label: 'Revision Number', value: detailFor.revisionNumber || '—' }, { label: 'Date', value: fmtDate(detailFor.date) },
          { label: 'Scope Applicable', value: detailFor.scopeApplicable || '—' }, { label: 'Added By', value: `${detailFor.addedBy} — ${fmtDate(detailFor.addedDate)}` },
        ] : []}
        attachments={detailFor && detailFor.fileUrl ? [{ name: detailFor.file, url: detailFor.fileUrl }] : []}
      />
    </Collapsible>
  );
}
function AddCatalogEntryModal({ open, onClose, ctx, onSubmit }) {
  const blank = { name: '', date: todayISO(), revisionNumber: '', scopeApplicable: '', file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() { if (!form.name.trim()) return; onSubmit(form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Catalog Entry" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. 2026 Hardware Catalog" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Revision Number"><TextInput value={form.revisionNumber} onChange={e => setForm({ ...form, revisionNumber: e.target.value })} /></Field>
        </div>
        <Field label="Scope Applicable">
          <Select value={form.scopeApplicable} onChange={e => setForm({ ...form, scopeApplicable: e.target.value })}>
            <option value="">— none —</option>
            {ctx.scopeLibrary.filter(f => f.active).map(f => <option key={f.id} value={f.name}>{f.name}</option>)}
          </Select>
        </Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
      </div>
    </Modal>
  );
}
// Only rendered at all when ctx.canSeeVendorPricing is true (Admin,
// Accounting, General Manager) — everyone else never sees this section
// exists, not just a disabled view of it.
function VendorPriceListSection({ ctx, vendor, isForwarder }) {
  const [showAdd, setShowAdd] = useState(false);
  const addFn = isForwarder ? ctx.addFreightForwarderPriceListEntry : ctx.addVendorPriceListEntry;
  const removeFn = isForwarder ? ctx.removeFreightForwarderPriceListEntry : ctx.removeVendorPriceListEntry;
  return (
    <Collapsible title="Price List" count={vendor.priceList.length} right={<Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add Price List</Button>}>
      <p className="text-xs text-[var(--leon-black)]/50 mb-2">Visible to Admin, Accounting, and General Manager only.</p>
      {vendor.priceList.length === 0 ? <EmptyState text="No price lists on file." /> : (
        <div className="space-y-1.5">
          {vendor.priceList.map(p => (
            <div key={p.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
              <div className="min-w-0">
                <p className="text-sm font-semibold">{p.name}{p.revisionNumber ? ` — Rev ${p.revisionNumber}` : ''}</p>
                <p className="text-xs text-[var(--leon-black)]/50">{fmtDate(p.date)} · Added by {p.addedBy}{p.notes ? ` — ${p.notes}` : ''}</p>
              </div>
              <div className="flex items-center gap-2 shrink-0">
                <AttachmentLink name={p.file} url={p.fileUrl} />
                <IconBtn title="Remove" onClick={() => removeFn(vendor.id, p.id)}>✕</IconBtn>
              </div>
            </div>
          ))}
        </div>
      )}
      <AddPriceListEntryModal open={showAdd} onClose={() => setShowAdd(false)} onSubmit={data => addFn(vendor.id, data)} />
    </Collapsible>
  );
}
function AddPriceListEntryModal({ open, onClose, onSubmit }) {
  const blank = { name: '', date: todayISO(), revisionNumber: '', file: '', fileUrl: null, notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() { if (!form.name.trim()) return; onSubmit(form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Price List" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. 2026 Wholesale Price List" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Revision Number"><TextInput value={form.revisionNumber} onChange={e => setForm({ ...form, revisionNumber: e.target.value })} /></Field>
        </div>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function PartyFinancialSummaryBlock({ ctx, summary, subcontractorMode }) {
  return (
    <div className="grid sm:grid-cols-4 gap-3 mb-5">
      {subcontractorMode ? (
        <>
          <StatBox label="Total Submitted" value={fmtMoney(summary.totalSubmitted)} />
          <StatBox label="Total Approved" value={fmtMoney(summary.totalApproved)} />
          <StatBox label="Total Paid" value={fmtMoney(summary.totalPaid)} />
          <StatBox label="Total Open Balance" value={fmtMoney(summary.totalOpenBalance)} tone={summary.totalOpenBalance > 0 ? 'yellow' : 'green'} />
          <StatBox label="Total Past Due" value={fmtMoney(summary.totalPastDue)} tone={summary.totalPastDue > 0 ? 'red' : 'green'} />
          <StatBox label="Rejected / Revision Requested" value={String(summary.rejectedRevisionCount)} tone={summary.rejectedRevisionCount > 0 ? 'red' : undefined} />
          <StatBox label="Open Invoice Count" value={String(summary.openCount)} />
          <StatBox label="Next Payment Due" value={summary.nextPaymentDue ? fmtDate(summary.nextPaymentDue) : '—'} />
        </>
      ) : (
        <>
          <StatBox label="Total Invoiced" value={fmtMoney(summary.totalInvoiced)} />
          <StatBox label="Total Paid" value={fmtMoney(summary.totalPaid)} />
          <StatBox label="Total Open Balance" value={fmtMoney(summary.totalOpenBalance)} tone={summary.totalOpenBalance > 0 ? 'yellow' : 'green'} />
          <StatBox label="Total Past Due" value={fmtMoney(summary.totalPastDue)} tone={summary.totalPastDue > 0 ? 'red' : 'green'} />
          <StatBox label="Open Invoices" value={String(summary.openCount)} />
          <StatBox label="Past Due Invoices" value={String(summary.pastDueCount)} tone={summary.pastDueCount > 0 ? 'red' : 'green'} />
          <StatBox label="Next Payment Due" value={summary.nextPaymentDue ? fmtDate(summary.nextPaymentDue) : '—'} />
        </>
      )}
    </div>
  );
}
function OpenInvoicesTable({ ctx, invoices, onOpen }) {
  if (invoices.length === 0) return <EmptyState text="No open invoices." />;
  return (
    <div className="overflow-x-auto">
      <table className="w-full text-xs">
        <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3">Invoice #</th><th className="py-1 pr-3">Project</th><th className="py-1 pr-3">Scope</th><th className="py-1 pr-3">Invoice Date</th><th className="py-1 pr-3">Due Date</th><th className="py-1 pr-3">Amount</th><th className="py-1 pr-3">Paid</th><th className="py-1 pr-3">Open Balance</th><th className="py-1 pr-3">Payment Status</th></tr></thead>
        <tbody>
          {invoices.map(inv => {
            const project = ctx.projects.find(p => p.id === inv.projectId);
            const scope = project && project.scopes.find(s => s.id === inv.scopeId);
            const pastDue = invoiceIsPastDue(inv);
            return (
              <tr key={inv.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => onOpen(inv.id)}>
                <td className="py-1.5 pr-3 font-semibold text-[var(--leon-brown)]">{inv.invoiceNumber}</td>
                <td className="py-1.5 pr-3">{project ? project.name : '—'}</td>
                <td className="py-1.5 pr-3">{scope ? scope.name : '—'}</td>
                <td className="py-1.5 pr-3">{fmtDate(inv.invoiceDate)}</td>
                <td className="py-1.5 pr-3">{fmtDate(inv.dueDate)}{pastDue && <span className="text-[var(--leon-red)] font-semibold"> ({invoiceDaysPastDue(inv)}d late)</span>}</td>
                <td className="py-1.5 pr-3">{fmtMoney(inv.amount)}</td>
                <td className="py-1.5 pr-3">{fmtMoney(invoiceTotalPaid(inv))}</td>
                <td className="py-1.5 pr-3 font-semibold">{fmtMoney(invoiceOpenBalance(inv))}</td>
                <td className="py-1.5 pr-3"><StatusBadge status={inv.paymentStatus} /></td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}
const VENDOR_DETAIL_SUBTABS = [
  { key: 'info', label: 'Vendor Info', icon: '📋' },
  { key: 'catalog', label: 'Catalog & Pricing', icon: '📚' },
  { key: 'invoices', label: 'Invoices', icon: '🧾' },
  { key: 'projects', label: 'Projects', icon: '🏗️' },
];
// The finish catalog we hold for this vendor — the same records the Selection
// Hub's picker searches, reached from the vendor rather than from the library,
// because "what can we get from them" is a question asked about a vendor.
function VendorFinishCatalogSection({ ctx, vendor }) {
  const keys = useMemo(() => vendorSupplierKeys(vendor.id), [vendor.id, ctx.supplierVendorLinks]);
  const cats = useMemo(() => supplierCategories().filter(c => keys.includes(c.sup)), [keys, ctx.supplierFinishOverrides]);
  const total = cats.reduce((n, c) => n + c.count, 0);
  const [openCat, setOpenCat] = useState('');
  const [q, setQ] = useState('');
  const results = useMemo(() => (openCat ? searchSupplierFinishes(keys[0], openCat, q, 24) : []), [keys, openCat, q]);
  return (
    <Collapsible id={`vendor-finishes-${vendor.id}`} title="Finish Catalog" count={total || undefined}
      right={keys.length ? <span className="text-[11px] text-[var(--leon-black)]/45">{cats.length} {cats.length === 1 ? 'category' : 'categories'}</span> : null}>
      {!keys.length ? (
        <p className="text-sm text-[var(--leon-black)]/45">
          No finish catalog is linked to this vendor. Link one in <b>LEON Collection &rarr; Supplier Finishes</b>,
          under <b>Suppliers &amp; vendors</b>.
        </p>
      ) : (
        <>
          <div className="flex items-center gap-2 flex-wrap mb-2">
            <Select value={openCat} onChange={e => { setOpenCat(e.target.value); setQ(''); }} className="!py-1 !text-xs !w-64">
              <option value="">— pick a category —</option>
              {cats.map(c => <option key={c.cat} value={c.cat}>{c.cat} ({c.count})</option>)}
            </Select>
            <TextInput value={q} onChange={e => setQ(e.target.value)} disabled={!openCat}
              placeholder={openCat ? 'Name or supplier code…' : 'Pick a category first'} className="!py-1 !text-xs !w-64" />
            <span className="text-xs text-[var(--leon-black)]/45">{total} finishes in total</span>
          </div>
          {openCat && (
            results.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40 italic">Nothing matches that.</p> : (
              <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-2">
                {results.map(r => (
                  <div key={r.sup + r.id} className="border border-[var(--leon-line)] rounded-lg overflow-hidden bg-white">
                    {r.img
                      ? <img src={r.img} alt="" className="w-full h-20 object-cover" />
                      : <div className="w-full h-20 bg-[var(--leon-cream)]" />}
                    <div className="px-1.5 py-1 leading-tight">
                      <div className="text-[11px] font-semibold truncate" title={r.name}>{r.name}</div>
                      <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{r.code}</div>
                    </div>
                  </div>
                ))}
              </div>
            )
          )}
        </>
      )}
    </Collapsible>
  );
}

function VendorDetailView({ ctx, vendorId, type }) {
  const list = type === 'forwarder' ? ctx.freightForwarders : ctx.vendors;
  const updateTerms = type === 'forwarder' ? ctx.updateFreightForwarderTerms : ctx.updateVendorTerms;
  const updateVendor = type === 'forwarder' ? ctx.updateFreightForwarder : ctx.updateVendor;
  const updateBilling = type === 'forwarder' ? ctx.updateFreightForwarderBilling : ctx.updateVendorBilling;
  const estimateKey = type === 'forwarder' ? 'freightEstimates' : 'vendorEstimates';
  const idKey = type === 'forwarder' ? 'forwarderId' : 'vendorId';
  const vendor = list.find(v => v.id === vendorId);
  const [sub, setSub] = useState('info');
  const [filter, setFilter] = useState('All');
  const [editingTerms, setEditingTerms] = useState(false);
  const [terms, setTerms] = useState(vendor ? vendor.defaultPaymentTerms : []);
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);

  const allInvoices = useMemo(() => allApInvoices(ctx.projects), [ctx.projects]);

  if (!vendor) return <EmptyState text="Not found." />;

  const projectsWithEstimates = ctx.projects
    .map(p => ({ project: p, estimates: (p[estimateKey] || []).filter(e => e[idKey] === vendor.id) }))
    .filter(x => x.estimates.length > 0);
  const filtered = projectsWithEstimates.filter(x => filter === 'All' || x.project.pipelineStatus === filter);
  const allEstimates = projectsWithEstimates.flatMap(x => x.estimates);
  const rate = vendorAcceptanceRate(allEstimates);
  const vendorInvoices = allInvoices.filter(i => i.vendorId === vendor.id);
  const summary = partyFinancialSummary(vendorInvoices);

  function exportVendorCsv() {
    downloadCsv(`${type === 'forwarder' ? 'forwarder' : 'vendor'}-${vendor.name}`,
      [{ key: 'name', label: 'Name' }, { key: 'vendorType', label: 'Type' }, { key: 'status', label: 'Status' }, { key: 'contactPerson', label: 'Contact' }, { key: 'phone', label: 'Phone' }, { key: 'email', label: 'Email' }, { key: 'address', label: 'Address' }],
      [vendor]
    );
  }
  return (
    <div>
      <button onClick={ctx.goVendors} className="no-print text-sm text-[var(--leon-brown)] font-semibold mb-3">← Back to {type === 'forwarder' ? 'Freight Forwarders' : 'Vendors'}</button>

      <div className="no-print flex justify-end gap-2 mb-2">
        <PrintButton onClick={() => window.print()} />
        <Button variant="outline" size="sm" onClick={exportVendorCsv}>Export CSV</Button>
      </div>
      <div className="print-only print-area p-8">
        <PrintDocHeader title={vendor.name} meta={vendor.vendorType} />
        <table className="w-full text-sm mb-4">
          <tbody>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold w-1/3">Status</td><td className="py-1">{vendor.status}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Contact</td><td className="py-1">{vendor.contactPerson}{vendor.contactTitle ? `, ${vendor.contactTitle}` : ''}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Phone / Email</td><td className="py-1">{vendor.phone} {vendor.mobile} {vendor.email}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Address</td><td className="py-1">{vendor.address}{vendor.city ? `, ${vendor.city}` : ''}{vendor.state ? `, ${vendor.state}` : ''} {vendor.zip}</td></tr>
          </tbody>
        </table>
      </div>

      <div className="no-print">
      <div className="bg-white border border-[var(--leon-line)] rounded-xl p-4 mb-4">
        <div className="flex items-start justify-between gap-3 flex-wrap">
          <div>
            <div className="flex items-center gap-2 flex-wrap">
              <h1 className="text-xl font-bold">{vendor.name}</h1>
              <Badge tone="neutral">{vendor.vendorType}</Badge>
              <Badge tone={vendor.status === 'Active' ? 'green' : vendor.status === 'On Hold' ? 'yellow' : 'neutral'}>{vendor.status}</Badge>
            </div>
            <p className="text-sm text-[var(--leon-black)]/50">{vendor.contactPerson}{vendor.contactTitle ? `, ${vendor.contactTitle}` : ''} · {vendor.phone}{vendor.mobile ? ` / ${vendor.mobile}` : ''} · {vendor.email}</p>
            {vendor.website && <p className="text-xs text-[var(--leon-black)]/40">{vendor.website}</p>}
            {vendor.address && <p className="text-xs text-[var(--leon-black)]/40 mt-1">{vendor.address}{vendor.city ? `, ${vendor.city}` : ''}{vendor.state ? `, ${vendor.state}` : ''} {vendor.zip}{vendor.country ? `, ${vendor.country}` : ''}</p>}
            {vendor.notes && <p className="text-xs text-[var(--leon-black)]/40 mt-1 italic">{vendor.notes}</p>}
          </div>
          <Select value={vendor.status} onChange={e => updateVendor(vendor.id, { status: e.target.value })} className="!w-32 !py-1 !text-xs">{VENDOR_STATUSES.map(s => <option key={s}>{s}</option>)}</Select>
        </div>
        <div className="flex items-center gap-2 flex-wrap mt-3 pt-3 border-t border-[var(--leon-line)]">
          <span className="text-xs font-semibold text-[var(--leon-black)]/50">Default Payment Terms:</span>
          {editingTerms ? (
            <>
              {terms.map((t, i) => (
                <span key={t.id} className="inline-flex items-center gap-1 text-xs border border-[var(--leon-line)] rounded-full px-2 py-0.5">
                  {t.label} <TextInput type="number" defaultValue={t.pct} onBlur={e => { const next = [...terms]; next[i] = { ...t, pct: Number(e.target.value) }; setTerms(next); }} className="!w-14 !py-0 !text-xs !border-0" />%
                </span>
              ))}
              <Button size="sm" onClick={() => { updateTerms(vendor.id, terms); setEditingTerms(false); }}>Save</Button>
            </>
          ) : (
            <>
              {vendor.defaultPaymentTerms.map(t => <Badge key={t.id} tone="neutral">{t.label} {t.pct}%</Badge>)}
              <button onClick={() => { setTerms(vendor.defaultPaymentTerms); setEditingTerms(true); }} className="text-xs text-[var(--leon-brown)] font-semibold">Edit</button>
            </>
          )}
        </div>
      </div>

      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {VENDOR_DETAIL_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />

      {sub === 'info' && (
        <>
          <VendorContactsBlock ctx={ctx} vendor={vendor} isForwarder={type === 'forwarder'} />
          <Collapsible title="Billing Information">
            {/* Bank and remittance details are what an invoice-fraud attempt
                needs. This is a saved record that is READ far more often than
                it is changed, so it opens locked. */}
            <EditLock canEdit={ctx.canEdit('vendors')} hint="Locked — press Edit to change this vendor's billing details.">
              <VendorBillingFields billing={vendor.billing} onChange={fields => updateBilling(vendor.id, fields)} />
            </EditLock>
          </Collapsible>
          <AttachmentsAndActivitySection
            attachments={vendor.attachments} activityLog={vendor.activityLog}
            onAdd={data => (type === 'forwarder' ? ctx.addFreightForwarderAttachment : ctx.addVendorAttachment)(vendor.id, data)}
            onRemove={attId => (type === 'forwarder' ? ctx.removeFreightForwarderAttachment : ctx.removeVendorAttachment)(vendor.id, attId)}
            editable={true}
          />
        </>
      )}

      {sub === 'catalog' && (
        <>
          {type !== 'forwarder' && <VendorFinishCatalogSection ctx={ctx} vendor={vendor} />}
          <VendorCatalogSection ctx={ctx} vendor={vendor} isForwarder={type === 'forwarder'} />
          {ctx.canSeeVendorPricing && <VendorPriceListSection ctx={ctx} vendor={vendor} isForwarder={type === 'forwarder'} />}
        </>
      )}

      {sub === 'invoices' && (
        <>
          <h2 className="font-bold text-sm mt-1 mb-2">Financial Summary</h2>
          <PartyFinancialSummaryBlock ctx={ctx} summary={summary} />
          <Collapsible title="Open Invoices" count={summary.openInvoices.length} right={<Button size="sm" onClick={() => setAddInvoice(true)}>+ Add Invoice</Button>}>
            <OpenInvoicesTable ctx={ctx} invoices={summary.openInvoices} onOpen={setOpenInvoice} />
          </Collapsible>
          <Collapsible title="All Invoices" count={vendorInvoices.length}>
            <OpenInvoicesTable ctx={ctx} invoices={vendorInvoices} onOpen={setOpenInvoice} />
          </Collapsible>
        </>
      )}

      {sub === 'projects' && (
        <>
          <div className="grid sm:grid-cols-3 gap-3 mb-5">
            <StatBox label="Projects Pricing" value={String(projectsWithEstimates.length)} />
            <StatBox label="Total Estimates" value={String(allEstimates.length)} />
            <StatBox label="PO Acceptance Rate" value={rate.pct === null ? '—' : fmtPct(rate.pct)} tone={rate.pct !== null && rate.pct < 50 ? 'red' : 'green'} />
          </div>

          <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)] flex-wrap">
            {['All', ...PIPELINE_STATUSES].map(s => (
              <button
                key={s}
                onClick={() => setFilter(s)}
                className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold whitespace-nowrap border-b-2 ${filter === s ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}
              >
                {s} {s !== 'All' && <span className="text-[var(--leon-black)]/30">({projectsWithEstimates.filter(x => x.project.pipelineStatus === s).length})</span>}
              </button>
            ))}
          </div>

          {filtered.length === 0 ? <EmptyState text="No jobs match." /> : (
            <div className="space-y-3">
              {filtered.map(({ project, estimates }) => (
                <div key={project.id} className="bg-white border border-[var(--leon-line)] rounded-xl p-4">
                  <div className="flex items-center justify-between gap-2 mb-2">
                    <button onClick={() => ctx.goProject(project.id)} className="text-sm font-bold text-[var(--leon-brown)] hover:underline">{project.name}</button>
                    <StatusBadge status={project.pipelineStatus} />
                  </div>
                  {estimates.map(e => {
                    const po = (type === 'forwarder' ? project.freightPOs : project.purchaseOrders).find(x => x.id === e.poId);
                    return (
                      <div key={e.id} className="border-t border-[var(--leon-line)] pt-2 mt-2 first:border-0 first:pt-0 first:mt-0">
                        <div className="flex items-center justify-between gap-2 flex-wrap">
                          <p className="text-sm font-semibold">{e.description} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(e.amount)}</span></p>
                          {po ? <Badge tone="green">{po.poNumber}</Badge> : <Badge tone="yellow">Pending Approval</Badge>}
                        </div>
                        <div className="flex items-center gap-1.5 mt-0.5"><Badge tone="neutral">{e.category || 'Original Order'}</Badge><span className="text-xs text-[var(--leon-black)]/40">{fmtDate(e.date)}</span></div>
                        {e.revisions && e.revisions.length > 0 && (
                          <div className="mt-1 space-y-0.5">
                            {e.revisions.map(r => (
                              <p key={r.id} className="text-[11px] text-[var(--leon-black)]/50">Rev {r.revision}: {fmtMoney(r.amount)} · {fmtDate(r.date)}{r.file ? ` · ${r.file}` : ''}</p>
                            ))}
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              ))}
            </div>
          )}
        </>
      )}
      </div>

      <AddApInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} partyType={type === 'forwarder' ? 'Freight' : 'Vendor'} party={vendor} />
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}

function SubcontractorDetailView({ ctx, subId }) {
  const sub = ctx.subcontractors.find(s => s.id === subId);
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);
  const [linkingProject, setLinkingProject] = useState('');
  const [showSendBack, setShowSendBack] = useState(false);
  const allInvoices = useMemo(() => allApInvoices(ctx.projects), [ctx.projects]);
  if (!sub) return <EmptyState text="Not found." />;
  const canApproveRegistration = ['Admin', 'Accounting'].includes(ctx.currentRole);
  const subInvoices = allInvoices.filter(i => i.vendorId === sub.id);
  const summary = partyFinancialSummary(subInvoices);
  const linkedProjects = sub.projectIds.map(id => ctx.projects.find(p => p.id === id)).filter(Boolean);

  function exportSubcontractorCsv() {
    downloadCsv(`subcontractor-${sub.companyName}`,
      [{ key: 'companyName', label: 'Company' }, { key: 'trade', label: 'Trade' }, { key: 'status', label: 'Status' }, { key: 'contactName', label: 'Contact' }, { key: 'phone', label: 'Phone' }, { key: 'email', label: 'Email' }, { key: 'address', label: 'Address' }],
      [sub]
    );
  }
  return (
    <div>
      <button onClick={ctx.goVendors} className="no-print text-sm text-[var(--leon-brown)] font-semibold mb-3">← Back to Subcontractors</button>

      <div className="no-print flex justify-end gap-2 mb-2">
        <PrintButton onClick={() => window.print()} />
        <Button variant="outline" size="sm" onClick={exportSubcontractorCsv}>Export CSV</Button>
      </div>
      <div className="print-only print-area p-8">
        <PrintDocHeader title={sub.companyName} meta={sub.trade} />
        <table className="w-full text-sm mb-4">
          <tbody>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold w-1/3">Status</td><td className="py-1">{sub.status}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Contact</td><td className="py-1">{sub.contactName}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Phone / Email</td><td className="py-1">{sub.phone} {sub.mobile} {sub.email}</td></tr>
            <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Address</td><td className="py-1">{sub.address}{sub.city ? `, ${sub.city}` : ''}{sub.state ? `, ${sub.state}` : ''} {sub.zip}</td></tr>
          </tbody>
        </table>
      </div>

      <div className="no-print">
      <div className="bg-white border border-[var(--leon-line)] rounded-xl p-4 mb-4">
        <div className="flex items-start justify-between gap-3 flex-wrap">
          <div>
            <div className="flex items-center gap-2 flex-wrap">
              <h1 className="text-xl font-bold">{sub.companyName}</h1>
              <Badge tone="neutral">{sub.trade}</Badge>
              <Badge tone={sub.status === 'Active' ? 'green' : 'neutral'}>{sub.status}</Badge>
            </div>
            <p className="text-sm text-[var(--leon-black)]/50">{sub.contactName} · {sub.phone}{sub.mobile ? ` / ${sub.mobile}` : ''} · {sub.email}</p>
            {sub.address && <p className="text-xs text-[var(--leon-black)]/40 mt-1">{sub.address}{sub.city ? `, ${sub.city}` : ''}{sub.state ? `, ${sub.state}` : ''} {sub.zip}</p>}
            {sub.emergencyContact && <p className="text-xs text-[var(--leon-black)]/40 mt-1">Emergency: {sub.emergencyContact}{sub.emergencyPhone ? ` — ${sub.emergencyPhone}` : ''}</p>}
            {sub.notes && <p className="text-xs text-[var(--leon-black)]/40 mt-1 italic">{sub.notes}</p>}
          </div>
          <Button size="sm" variant="ghost" onClick={() => ctx.setSubcontractorActive(sub.id, sub.status !== 'Active')}>{sub.status === 'Active' ? 'Deactivate' : 'Activate'}</Button>
        </div>
      </div>

      <Collapsible title="Registration" right={<StatusBadge status={sub.registrationStatus} />}>
        <div className="grid sm:grid-cols-2 gap-3 mb-3">
          <Field label="W9"><FileField name={sub.w9File} url={sub.w9FileUrl} editable={false} onChange={() => {}} /></Field>
          <Field label="Certificate of Insurance (COI)"><FileField name={sub.coiFile} url={sub.coiFileUrl} editable={false} onChange={() => {}} /></Field>
        </div>
        {sub.registrationStatus === 'Submitted' && (
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">Submitted by {sub.submittedBy} on {fmtDate(sub.submittedDate)}.</p>
        )}
        {sub.registrationStatus === 'Approved' && (
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">Approved by {sub.approvedBy} on {fmtDate(sub.approvedDate)}.</p>
        )}
        {sub.registrationStatus === 'Edit Requested' && (
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">Edit requested {fmtDate(sub.editRequestedDate)}{sub.editRequestNote ? ` — ${sub.editRequestNote}` : ''}.</p>
        )}
        {canApproveRegistration && (
          <div className="flex items-center gap-2 flex-wrap mb-3">
            {sub.registrationStatus === 'Submitted' && (
              <>
                <Button size="sm" onClick={() => ctx.approveSubcontractorRegistration(sub.id)}>Approve Registration</Button>
                <Button size="sm" variant="ghost" onClick={() => setShowSendBack(true)}>Send Back for Changes</Button>
              </>
            )}
            {sub.registrationStatus === 'Edit Requested' && (
              <Button size="sm" onClick={() => ctx.releaseSubcontractorForEditing(sub.id)}>Release for Editing</Button>
            )}
          </div>
        )}
        {sub.registrationHistory.length > 0 && (
          <div className="space-y-1">
            {[...sub.registrationHistory].reverse().map(h => (
              <p key={h.id} className="text-xs text-[var(--leon-black)]/60">{fmtDate(h.date)} {h.time} — {h.user}: {h.action}{h.notes ? ` — ${h.notes}` : ''}</p>
            ))}
          </div>
        )}
      </Collapsible>
      <SendSubcontractorBackModal open={showSendBack} onClose={() => setShowSendBack(false)} ctx={ctx} sub={sub} />

      <Collapsible title="Billing Information">
        <EditLock canEdit={ctx.canEdit('vendors')} hint="Locked — press Edit to change this subcontractor's billing details.">
          <VendorBillingFields billing={sub.billing} onChange={fields => ctx.updateSubcontractorBilling(sub.id, fields)} />
        </EditLock>
      </Collapsible>

      <Collapsible title="Connected Projects" count={linkedProjects.length}>
        <div className="flex items-center gap-2 mb-2">
          <Select value={linkingProject} onChange={e => setLinkingProject(e.target.value)} className="!w-64">
            <option value="">Link a project…</option>
            {ctx.projects.filter(p => !sub.projectIds.includes(p.id)).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
          <Button size="sm" variant="ghost" disabled={!linkingProject} onClick={() => { ctx.linkSubcontractorProject(sub.id, linkingProject); setLinkingProject(''); }}>Link</Button>
        </div>
        {linkedProjects.length === 0 ? <EmptyState text="Not connected to any projects yet." /> : (
          <div className="space-y-1.5">
            {linkedProjects.map(p => (
              <div key={p.id} className="flex items-center justify-between border border-[var(--leon-line)] rounded-md px-3 py-2 text-sm">
                <button onClick={() => ctx.goProject(p.id)} className="font-semibold text-[var(--leon-brown)] hover:underline">{p.name}</button>
                <IconBtn title="Unlink" onClick={() => ctx.unlinkSubcontractorProject(sub.id, p.id)}>✕</IconBtn>
              </div>
            ))}
          </div>
        )}
      </Collapsible>

      <h2 className="font-bold text-sm mt-5 mb-2">Financial Summary</h2>
      <PartyFinancialSummaryBlock ctx={ctx} summary={summary} subcontractorMode />

      <Collapsible title="Open Invoices" count={summary.openInvoices.length} right={<Button size="sm" onClick={() => setAddInvoice(true)}>+ Add Invoice</Button>}>
        <OpenInvoicesTable ctx={ctx} invoices={summary.openInvoices} onOpen={setOpenInvoice} />
      </Collapsible>
      <Collapsible title="All Invoices & Payment History" count={subInvoices.length}>
        <OpenInvoicesTable ctx={ctx} invoices={subInvoices} onOpen={setOpenInvoice} />
      </Collapsible>

      <AttachmentsAndActivitySection
        attachments={sub.attachments} activityLog={sub.activityLog}
        onAdd={data => ctx.addSubcontractorAttachment(sub.id, data)} onRemove={attId => ctx.removeSubcontractorAttachment(sub.id, attId)}
        editable={true}
      />
      </div>

      <AddSubcontractorInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} subcontractor={sub} />
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}
function SendSubcontractorBackModal({ open, onClose, ctx, sub }) {
  const [note, setNote] = useState('');
  useEffect(() => { if (open) setNote(''); }, [open]);
  function submit() { ctx.requestSubcontractorChanges(sub.id, note); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Send Back for Changes" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Send Back</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Unlocks their registration for editing and lets them know what to fix.</p>
        <Field label="What needs to change?"><TextArea rows={3} value={note} onChange={e => setNote(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
function AddSubcontractorModal({ open, onClose, ctx }) {
  const blank = { companyName: '', contactName: '', trade: SUBCONTRACTOR_TRADES[0], phone: '', mobile: '', email: '', address: '', city: '', state: '', zip: '', emergencyContact: '', emergencyPhone: '', notes: '', username: '', password: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.companyName.trim()) return;
    ctx.addSubcontractor(form);
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title="New Subcontractor" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Subcontractor</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Company / Subcontractor Name"><TextInput value={form.companyName} onChange={e => setForm({ ...form, companyName: e.target.value })} /></Field>
          <Field label="Individual Contact Name"><TextInput value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></Field>
        </div>
        <Field label="Trade / Service"><Select value={form.trade} onChange={e => setForm({ ...form, trade: e.target.value })}>{SUBCONTRACTOR_TRADES.map(t => <option key={t}>{t}</option>)}</Select></Field>
        <Field label="Main office"><Select value={form.officeLocation || ''} onChange={e => setForm({ ...form, officeLocation: e.target.value })}><option value="">— not set —</option>{LEON_OFFICES.map(o => <option key={o.key} value={o.key}>{o.flag} {o.city}, {o.country}</option>)}</Select></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
          <Field label="Mobile"><TextInput value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} /></Field>
          <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
        </div>
        <Field label="Address"><TextInput value={form.address} onChange={e => setForm({ ...form, address: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="City"><TextInput value={form.city} onChange={e => setForm({ ...form, city: e.target.value })} /></Field>
          <Field label="State"><TextInput value={form.state} onChange={e => setForm({ ...form, state: e.target.value })} /></Field>
          <Field label="ZIP Code"><TextInput value={form.zip} onChange={e => setForm({ ...form, zip: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Emergency Contact"><TextInput value={form.emergencyContact} onChange={e => setForm({ ...form, emergencyContact: e.target.value })} /></Field>
          <Field label="Emergency Phone"><TextInput value={form.emergencyPhone} onChange={e => setForm({ ...form, emergencyPhone: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 pt-2">Portal Login (optional)</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Username" hint="Leave blank if this subcontractor won't use the portal."><TextInput value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} /></Field>
          <Field label="Password"><TextInput type="password" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} /></Field>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================================
// Users — Admin/Accounting add and manage employee accounts
// ============================================================================
// ============================================================================
// Accounts Payable — invoice submission, approval, and payment (§ AP request)
// One connected workflow across Vendor / Freight / Subcontractor invoices.
// Approval Status and Payment Status are always shown and edited separately.
// ============================================================================
function AddApInvoiceModal({ open, onClose, ctx, partyType: fixedPartyType, party: fixedParty, defaultProjectId }) {
  // When no party is fixed (e.g. adding from a project's own Accounts
  // Payable sub-tab, rather than from one vendor's own page), a Vendor
  // Type + Vendor picker renders right in this form instead of a separate
  // modal — nesting one Modal inside another breaks the overlay.
  const pickable = !fixedParty;
  const partyLists = { Vendor: ctx.vendors, Freight: ctx.freightForwarders, Subcontractor: ctx.subcontractors };
  const [pickedType, setPickedType] = useState(fixedPartyType || 'Vendor');
  const [pickedId, setPickedId] = useState('');
  useEffect(() => { if (open && pickable) { setPickedType('Vendor'); setPickedId(ctx.vendors[0]?.id || ''); } }, [open]);
  const pickedList = partyLists[pickedType] || [];
  const pickedParty = pickable ? pickedList.find(p => p.id === pickedId) : null;
  const partyType = fixedPartyType || pickedType;
  const party = fixedParty || (pickedParty ? { id: pickedParty.id, name: pickedParty.name || pickedParty.companyName } : null);

  const relevantProjects = partyType === 'Subcontractor' && party ? ctx.projects.filter(p => (ctx.subcontractors.find(s => s.id === party.id)?.projectIds || []).includes(p.id)) : ctx.projects;
  const blank = { projectId: defaultProjectId || relevantProjects[0]?.id || '', scopeId: '', invoiceNumber: '', invoiceDate: todayISO(), dueDate: '', poReference: '', amount: '', currency: 'USD', file: '', fileUrl: null, description: '', servicePeriod: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, projectId: defaultProjectId || relevantProjects[0]?.id || '' }); }, [open, pickedId]);
  const project = ctx.projects.find(p => p.id === form.projectId);
  function submit() {
    if (!party || !form.invoiceNumber.trim() || !form.amount || !form.projectId) return;
    ctx.addApInvoice(form.projectId, { ...form, partyType, vendorId: party.id, vendorName: party.name });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title={party ? `Submit Invoice — ${party.name}` : 'Add Accounts Payable Invoice'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!party}>Submit Invoice</Button></>}>
      <div className="space-y-3">
        {pickable && (
          <div className="grid grid-cols-2 gap-3">
            <Field label="Vendor Type"><Select value={pickedType} onChange={e => { setPickedType(e.target.value); setPickedId((partyLists[e.target.value] || [])[0]?.id || ''); }}>{AP_PARTY_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
            <Field label={pickedType === 'Subcontractor' ? 'Subcontractor' : 'Vendor'}>
              <Select value={pickedId} onChange={e => setPickedId(e.target.value)}>
                {pickedList.length === 0 && <option value="">— none —</option>}
                {pickedList.map(p => <option key={p.id} value={p.id}>{p.name || p.companyName}</option>)}
              </Select>
            </Field>
          </div>
        )}
        <div className="grid grid-cols-2 gap-3">
          <Field label="Invoice Number"><TextInput value={form.invoiceNumber} onChange={e => setForm({ ...form, invoiceNumber: e.target.value })} /></Field>
          <Field label="PO / PI / Work Order Reference"><TextInput value={form.poReference} onChange={e => setForm({ ...form, poReference: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Project"><Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>{relevantProjects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
          <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{(project ? project.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Invoice Date"><TextInput type="date" value={form.invoiceDate} onChange={e => setForm({ ...form, invoiceDate: e.target.value })} /></Field>
          <Field label="Due Date"><TextInput type="date" value={form.dueDate} onChange={e => setForm({ ...form, dueDate: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Invoice Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Currency"><Select value={form.currency} onChange={e => setForm({ ...form, currency: e.target.value })}>{CURRENCIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
        </div>
        {partyType === 'Subcontractor' && <Field label="Service Period"><TextInput value={form.servicePeriod} onChange={e => setForm({ ...form, servicePeriod: e.target.value })} placeholder="e.g. 2026-03-01 to 2026-03-15" /></Field>}
        <Field label="Description of Work"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <p className="text-[11px] text-[var(--leon-black)]/40">After submission, this invoice's approval status becomes <strong>Submitted — Pending Approval</strong>.</p>
      </div>
    </Modal>
  );
}
// Subcontractor invoices can cover several projects at once (§ subcontractor
// multi-project invoice request) — one line item per project/scope, split
// on submit into one AP invoice record per project (apInvoices lives inside
// each project, so a single shared record isn't possible) but sharing an
// invoiceGroupId so they're still recognizable as one invoice everywhere
// they're shown (ApInvoiceDetailModal looks up its group-mates by that id).
function AddSubcontractorInvoiceModal({ open, onClose, ctx, subcontractor }) {
  const relevantProjects = ctx.projects.filter(p => (subcontractor.projectIds || []).includes(p.id));
  const blankLine = () => ({ id: uid('subline'), projectId: relevantProjects[0]?.id || '', scopeId: '', description: '', amount: '' });
  const [invoiceNumber, setInvoiceNumber] = useState('');
  const [invoiceDate, setInvoiceDate] = useState(todayISO());
  const [dueDate, setDueDate] = useState('');
  const [file, setFile] = useState('');
  const [fileUrl, setFileUrl] = useState(null);
  const [notes, setNotes] = useState('');
  const [lines, setLines] = useState([blankLine()]);
  useEffect(() => { if (open) { setInvoiceNumber(''); setInvoiceDate(todayISO()); setDueDate(''); setFile(''); setFileUrl(null); setNotes(''); setLines([blankLine()]); } }, [open]);
  function updateLine(id, fields) { setLines(prev => prev.map(l => l.id === id ? { ...l, ...fields } : l)); }
  function addLine() { setLines(prev => [...prev, blankLine()]); }
  function removeLine(id) { setLines(prev => prev.length > 1 ? prev.filter(l => l.id !== id) : prev); }
  const total = lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
  const projectsTouched = [...new Set(lines.map(l => l.projectId).filter(Boolean))];
  function submit() {
    if (!invoiceNumber.trim() || lines.some(l => !l.projectId || !l.amount)) return;
    const groupId = projectsTouched.length > 1 ? uid('apgroup') : null;
    const byProject = {};
    lines.forEach(l => { (byProject[l.projectId] = byProject[l.projectId] || []).push(l); });
    Object.entries(byProject).forEach(([projectId, projectLines]) => {
      const amount = projectLines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
      const description = projectLines.map(l => l.description).filter(Boolean).join('; ');
      // Route to THIS project's Sales Person — a multi-project invoice is split
      // per project, so each part goes to whoever owns that job.
      const proj = ctx.projects.find(p => p.id === projectId);
      const salesApproverId = proj ? teamMemberFor(proj, 'Sales Person') : null;
      ctx.addApInvoice(projectId, {
        partyType: 'Subcontractor', vendorId: subcontractor.id, vendorName: subcontractor.companyName,
        salesApproverId,
        invoiceNumber, invoiceDate, dueDate, amount, description, file, fileUrl, notes,
        lines: projectLines.map(l => ({ scopeId: l.scopeId || null, description: l.description, amount: Number(l.amount) || 0 })),
        invoiceGroupId: groupId,
      });
    });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title={`Submit Invoice — ${subcontractor.companyName}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit Invoice</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Invoice Number"><TextInput value={invoiceNumber} onChange={e => setInvoiceNumber(e.target.value)} /></Field>
          <Field label="Invoice Date"><TextInput type="date" value={invoiceDate} onChange={e => setInvoiceDate(e.target.value)} /></Field>
          <Field label="Due Date"><TextInput type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} /></Field>
        </div>
        <Field label="Line Items" hint="Add one line per project/scope this invoice covers — it's filed automatically under each project.">
          <div className="space-y-2 border border-[var(--leon-line)] rounded-lg p-2">
            {lines.map(l => {
              const lp = relevantProjects.find(p => p.id === l.projectId);
              return (
                <div key={l.id} className="border border-[var(--leon-line)] rounded-lg p-2 space-y-1.5">
                  <div className="grid grid-cols-2 gap-2">
                    <Select value={l.projectId} onChange={e => updateLine(l.id, { projectId: e.target.value, scopeId: '' })} className="!py-1 !text-xs">
                      {relevantProjects.length === 0 && <option value="">— no assigned projects —</option>}
                      {relevantProjects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                    <Select value={l.scopeId} onChange={e => updateLine(l.id, { scopeId: e.target.value })} className="!py-1 !text-xs">
                      <option value="">— scope —</option>
                      {(lp ? lp.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
                    </Select>
                  </div>
                  <div className="flex items-center gap-2">
                    <TextInput placeholder="Description" value={l.description} onChange={e => updateLine(l.id, { description: e.target.value })} className="!py-1 !text-xs flex-1" />
                    <TextInput type="number" placeholder="Amount" value={l.amount} onChange={e => updateLine(l.id, { amount: e.target.value })} className="!py-1 !text-xs !w-32" />
                    {lines.length > 1 && <IconBtn title="Remove line" onClick={() => removeLine(l.id)}>✕</IconBtn>}
                  </div>
                </div>
              );
            })}
            <button type="button" onClick={addLine} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Line Item</button>
          </div>
        </Field>
        {projectsTouched.length > 1 && <p className="text-xs text-[var(--leon-brown)] font-semibold">This invoice covers {projectsTouched.length} projects — it will be filed as a linked AP record under each one.</p>}
        <p className="text-sm font-semibold text-right">Total: {fmtMoney(total)}</p>
        <Field label="Attachment"><FileField name={file} url={fileUrl} onChange={(fname, url) => { setFile(fname); setFileUrl(url); }} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
// Freight invoices (§ freight invoice request) — one form, any number of
// project/scope/charge-type line items, split on submit (ctx.
// addFreightInvoice) into one linked AP record per project and allocated
// straight into each scope's actual cost for profitability.
function AddFreightInvoiceModal({ open, onClose, ctx }) {
  const blankLine = () => ({ id: uid('frline'), projectId: ctx.projects[0]?.id || '', scopeId: '', chargeType: FREIGHT_CHARGE_TYPES[0]?.key || '', description: '', amount: '' });
  const [partyId, setPartyId] = useState('');
  const [invoiceNumber, setInvoiceNumber] = useState('');
  const [invoiceDate, setInvoiceDate] = useState(todayISO());
  const [dueDate, setDueDate] = useState('');
  const [file, setFile] = useState('');
  const [fileUrl, setFileUrl] = useState(null);
  const [notes, setNotes] = useState('');
  const [lines, setLines] = useState([blankLine()]);
  const [newParty, setNewParty] = useState(false);
  const [newPartyName, setNewPartyName] = useState('');
  const [newPartyEmail, setNewPartyEmail] = useState('');
  const [newPartyPhone, setNewPartyPhone] = useState('');
  useEffect(() => {
    if (!open) return;
    setPartyId(ctx.freightForwarders[0]?.id || '');
    setInvoiceNumber(''); setInvoiceDate(todayISO()); setDueDate(''); setFile(''); setFileUrl(null); setNotes(''); setLines([blankLine()]);
    setNewParty(false); setNewPartyName(''); setNewPartyEmail(''); setNewPartyPhone('');
  }, [open]);
  const party = ctx.freightForwarders.find(f => f.id === partyId);
  function updateLine(id, fields) { setLines(prev => prev.map(l => l.id === id ? { ...l, ...fields } : l)); }
  function addLine() { setLines(prev => [...prev, blankLine()]); }
  function removeLine(id) { setLines(prev => prev.length > 1 ? prev.filter(l => l.id !== id) : prev); }
  const total = lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
  const projectsTouched = [...new Set(lines.map(l => l.projectId).filter(Boolean))];
  function saveNewParty() {
    if (!newPartyName.trim()) return;
    const f = ctx.addFreightForwarder({ name: newPartyName.trim(), contactPerson: '', phone: newPartyPhone, email: newPartyEmail, address: '' });
    setPartyId(f.id);
    setNewParty(false);
  }
  function submit() {
    if (!party || !invoiceNumber.trim() || lines.some(l => !l.projectId || !l.chargeType || !l.amount)) return;
    ctx.addFreightInvoice({ partyId: party.id, partyName: party.name, invoiceNumber, invoiceDate, dueDate, file, fileUrl, notes, lines });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title="Add Freight Invoice" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!party}>Submit Invoice</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Freight Company">
            {newParty ? (
              <div className="border border-[var(--leon-line)] rounded-lg p-2 space-y-2">
                <TextInput autoFocus placeholder="Company name" value={newPartyName} onChange={e => setNewPartyName(e.target.value)} className="!py-1 !text-xs" />
                <div className="grid grid-cols-2 gap-2">
                  <TextInput placeholder="Phone" value={newPartyPhone} onChange={e => setNewPartyPhone(e.target.value)} className="!py-1 !text-xs" />
                  <TextInput placeholder="Email" value={newPartyEmail} onChange={e => setNewPartyEmail(e.target.value)} className="!py-1 !text-xs" />
                </div>
                <div className="flex justify-end gap-2"><Button size="sm" variant="ghost" onClick={() => setNewParty(false)}>Cancel</Button><Button size="sm" onClick={saveNewParty}>Save</Button></div>
              </div>
            ) : (
              <div className="flex items-center gap-2">
                <Select value={partyId} onChange={e => setPartyId(e.target.value)}>
                  {ctx.freightForwarders.length === 0 && <option value="">— none on file —</option>}
                  {ctx.freightForwarders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
                </Select>
                <button type="button" onClick={() => setNewParty(true)} className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap">+ New</button>
              </div>
            )}
          </Field>
          <Field label="Invoice Number"><TextInput value={invoiceNumber} onChange={e => setInvoiceNumber(e.target.value)} /></Field>
          <Field label="Invoice Date"><TextInput type="date" value={invoiceDate} onChange={e => setInvoiceDate(e.target.value)} /></Field>
        </div>
        <Field label="Due Date"><TextInput type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} className="!w-40" /></Field>
        <Field label="Line Items" hint="One line per project, scope, and charge type — each amount is allocated straight into that scope's actual cost.">
          <div className="space-y-2 border border-[var(--leon-line)] rounded-lg p-2">
            {lines.map(l => {
              const lp = ctx.projects.find(p => p.id === l.projectId);
              return (
                <div key={l.id} className="border border-[var(--leon-line)] rounded-lg p-2 space-y-1.5">
                  <div className="grid grid-cols-3 gap-2">
                    <Select value={l.projectId} onChange={e => updateLine(l.id, { projectId: e.target.value, scopeId: '' })} className="!py-1 !text-xs">
                      {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                    <Select value={l.scopeId} onChange={e => updateLine(l.id, { scopeId: e.target.value })} className="!py-1 !text-xs">
                      <option value="">— scope —</option>
                      {(lp ? lp.scopes : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
                    </Select>
                    <Select value={l.chargeType} onChange={e => updateLine(l.id, { chargeType: e.target.value })} className="!py-1 !text-xs">
                      {FREIGHT_CHARGE_TYPES.map(f => <option key={f.key} value={f.key}>{f.label}</option>)}
                    </Select>
                  </div>
                  <div className="flex items-center gap-2">
                    <TextInput placeholder="Description" value={l.description} onChange={e => updateLine(l.id, { description: e.target.value })} className="!py-1 !text-xs flex-1" />
                    <TextInput type="number" placeholder="Amount" value={l.amount} onChange={e => updateLine(l.id, { amount: e.target.value })} className="!py-1 !text-xs !w-32" />
                    {lines.length > 1 && <IconBtn title="Remove line" onClick={() => removeLine(l.id)}>✕</IconBtn>}
                  </div>
                </div>
              );
            })}
            <button type="button" onClick={addLine} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Line Item</button>
          </div>
        </Field>
        {projectsTouched.length > 1 && <p className="text-xs text-[var(--leon-brown)] font-semibold">This invoice covers {projectsTouched.length} projects — it will be filed as a linked AP record under each one.</p>}
        <p className="text-sm font-semibold text-right">Total: {fmtMoney(total)}</p>
        <Field label="Attachment"><FileField name={file} url={fileUrl} onChange={(fname, url) => { setFile(fname); setFileUrl(url); }} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={notes} onChange={e => setNotes(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
// Miscellaneous job expenses (§ misc invoice request) — never touch
// procurement (no vendor estimate/PO/PI), so Accounting can log a payee and
// invoice straight here, including creating a brand-new vendor inline
// (vendorType 'Service Provider') without leaving the modal. Always feeds
// unplannedCostItems (lib.jsx) via partyType 'Miscellaneous', and — if a
// scope is picked — the scope's actual cost too (ctx.addMiscInvoice).
function AddMiscInvoiceModal({ open, onClose, ctx, project }) {
  const blank = { vendorId: '', scopeId: '', expenseCategory: MISC_EXPENSE_CATEGORIES[0], invoiceNumber: '', invoiceDate: todayISO(), dueDate: '', amount: '', recoverability: 'Pending Determination', description: '', file: '', fileUrl: null, notes: '' };
  const [form, setForm] = useState(blank);
  const [newVendor, setNewVendor] = useState(false);
  const [newVendorName, setNewVendorName] = useState('');
  const [newVendorEmail, setNewVendorEmail] = useState('');
  const [newVendorPhone, setNewVendorPhone] = useState('');
  useEffect(() => { if (open) { setForm({ ...blank, vendorId: ctx.vendors[0]?.id || '' }); setNewVendor(false); setNewVendorName(''); setNewVendorEmail(''); setNewVendorPhone(''); } }, [open]);
  function saveNewVendor() {
    if (!newVendorName.trim()) return;
    const v = ctx.addVendor({ name: newVendorName.trim(), contactPerson: '', phone: newVendorPhone, email: newVendorEmail, address: '', vendorType: 'Service Provider' });
    setForm(f => ({ ...f, vendorId: v.id }));
    setNewVendor(false);
  }
  function submit() {
    if (!form.vendorId || !form.invoiceNumber.trim() || !form.amount) return;
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    ctx.addMiscInvoice(project.id, { ...form, vendorName: vendor ? vendor.name : '' });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title="Add Miscellaneous Invoice" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit Invoice</Button></>}>
      <div className="space-y-3">
        <Field label="Vendor / Payee">
          {newVendor ? (
            <div className="border border-[var(--leon-line)] rounded-lg p-2 space-y-2">
              <TextInput autoFocus placeholder="Company or payee name" value={newVendorName} onChange={e => setNewVendorName(e.target.value)} className="!py-1 !text-xs" />
              <div className="grid grid-cols-2 gap-2">
                <TextInput placeholder="Phone" value={newVendorPhone} onChange={e => setNewVendorPhone(e.target.value)} className="!py-1 !text-xs" />
                <TextInput placeholder="Email" value={newVendorEmail} onChange={e => setNewVendorEmail(e.target.value)} className="!py-1 !text-xs" />
              </div>
              <div className="flex justify-end gap-2"><Button size="sm" variant="ghost" onClick={() => setNewVendor(false)}>Cancel</Button><Button size="sm" onClick={saveNewVendor}>Save Vendor</Button></div>
            </div>
          ) : (
            <div className="flex items-center gap-2">
              <Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}>
                {ctx.vendors.length === 0 && <option value="">— none on file —</option>}
                {ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
              </Select>
              <button type="button" onClick={() => setNewVendor(true)} className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap">+ New Vendor</button>
            </div>
          )}
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Expense Category"><Select value={form.expenseCategory} onChange={e => setForm({ ...form, expenseCategory: e.target.value })}>{MISC_EXPENSE_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
          <Field label="Scope (optional)" hint="Also adds this amount to that scope's actual cost.">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
              <option value="">— project-wide —</option>
              {project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Invoice Number"><TextInput value={form.invoiceNumber} onChange={e => setForm({ ...form, invoiceNumber: e.target.value })} /></Field>
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Invoice Date"><TextInput type="date" value={form.invoiceDate} onChange={e => setForm({ ...form, invoiceDate: e.target.value })} /></Field>
          <Field label="Due Date"><TextInput type="date" value={form.dueDate} onChange={e => setForm({ ...form, dueDate: e.target.value })} /></Field>
        </div>
        <Field label="Recoverability"><Select value={form.recoverability} onChange={e => setForm({ ...form, recoverability: e.target.value })}>{RECOVERABILITY_STATUSES.map(r => <option key={r}>{r}</option>)}</Select></Field>
        <Field label="Description"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function RecordPaymentModal({ open, onClose, ctx, invoice, projectId }) {
  const blank = { date: todayISO(), amount: '', method: PAYMENT_METHODS[0], reference: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  if (!invoice) return null;
  const openBalance = invoiceOpenBalance(invoice);
  function submit() {
    if (!form.amount || Number(form.amount) <= 0) return;
    ctx.addApPayment(projectId, invoice.id, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Record Payment — Invoice #${invoice.invoiceNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Record Payment</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Open balance: <strong>{fmtMoney(openBalance)}</strong> of {fmtMoney(invoice.amount)}. This adds a new payment — prior payments are never overwritten.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Payment Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Payment Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Payment Method"><Select value={form.method} onChange={e => setForm({ ...form, method: e.target.value })}>{PAYMENT_METHODS.map(m => <option key={m}>{m}</option>)}</Select></Field>
          <Field label="Payment Reference / Transaction #"><TextInput value={form.reference} onChange={e => setForm({ ...form, reference: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function RejectInvoiceModal({ open, onClose, ctx, invoice, projectId, targetStatus }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  if (!invoice) return null;
  function submit() {
    ctx.setApInvoiceApproval(projectId, invoice.id, targetStatus, reason);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`${targetStatus} — Invoice #${invoice.invoiceNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit}>{targetStatus}</Button></>}>
      <Field label="Reason"><TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} placeholder="Required — visible to the submitter." /></Field>
    </Modal>
  );
}
function ApInvoiceDetailModal({ invoiceId, onClose, ctx }) {
  const [payModal, setPayModal] = useState(false);
  const [rejectModal, setRejectModal] = useState(null);
  // Re-resolved from live ctx.projects on every render (rather than trusting
  // a snapshot passed in at open-time) so recording a payment or changing
  // approval status while this modal is open shows up immediately instead
  // of a stale view.
  const invoice = useMemo(() => invoiceId ? allApInvoices(ctx.projects).find(i => i.id === invoiceId) : null, [invoiceId, ctx.projects]);
  if (!invoice) return null;
  const project = ctx.projects.find(p => p.id === invoice.projectId);
  const scope = project && project.scopes.find(s => s.id === invoice.scopeId);
  const groupMates = invoice.invoiceGroupId ? allApInvoices(ctx.projects).filter(i => i.invoiceGroupId === invoice.invoiceGroupId && i.id !== invoice.id) : [];
  const openBalance = invoiceOpenBalance(invoice);
  const canApprove = ctx.canApproveInvoices && ['Submitted', 'Pending Approval', 'Revision Requested'].includes(invoice.approvalStatus);
  const canPay = ctx.canSeeFin && invoice.approvalStatus === 'Approved' && openBalance > 0;

  function exportInvoiceCsv() {
    downloadCsv(`invoice-${invoice.invoiceNumber}`,
      [{ key: 'invoiceNumber', label: 'Invoice #' }, { key: 'vendorName', label: 'Vendor' }, { key: 'partyType', label: 'Type' },
       { key: 'projectName', label: 'Project' }, { key: 'scopeName', label: 'Scope' }, { key: 'poReference', label: 'PO/PI Ref' },
       { key: 'invoiceDate', label: 'Invoice Date', type: 'date' }, { key: 'dueDate', label: 'Due Date', type: 'date' },
       { key: 'approvalStatus', label: 'Approval Status' }, { key: 'paymentStatus', label: 'Payment Status' },
       { key: 'amount', label: 'Amount', type: 'money' }, { key: 'paid', label: 'Paid', type: 'money' }, { key: 'openBalance', label: 'Open Balance', type: 'money' }],
      [{ ...invoice, projectName: project ? project.name : '', scopeName: scope ? scope.name : '', paid: invoiceTotalPaid(invoice), openBalance }]
    );
  }
  return (
    <>
      <Modal wide open={!!invoice && !payModal && !rejectModal} onClose={onClose} title={`Invoice #${invoice.invoiceNumber}`} footer={<><PrintButton onClick={() => window.print()} label="Print / PDF" /><Button variant="outline" onClick={exportInvoiceCsv}>Export CSV</Button><Button variant="ghost" onClick={onClose}>Close</Button></>}>
        <div className="space-y-4">
          <div className="grid grid-cols-2 gap-3 text-sm">
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Vendor / Subcontractor</span><span className="font-semibold">{invoice.vendorName}</span></div>
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Vendor Type</span><span className="font-semibold">{invoice.partyType}</span></div>
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Project</span><span className="font-semibold">{project ? project.name : '—'}</span></div>
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Scope</span><span className="font-semibold">{scope ? scope.name : '—'}</span></div>
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">PO / PI / Work Order Ref</span><span className="font-semibold">{invoice.poReference || '—'}</span></div>
            <div><span className="text-xs text-[var(--leon-black)]/40 uppercase block">Invoice / Due Date</span><span className="font-semibold">{fmtDate(invoice.invoiceDate)} / {fmtDate(invoice.dueDate)}</span></div>
          </div>
          {/* The planned payment date drives the financial calendar, so every
              time it moves it is recorded here rather than just overwritten. */}
          {(invoice.dueDateHistory || []).length > 0 && (
            <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-[var(--leon-cream)]/50">
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">Planned payment date</p>
              <p className="text-sm">
                <b>{fmtDate(invoice.dueDate)}</b>
                {invoice.dueDateOriginal && invoice.dueDateOriginal !== invoice.dueDate &&
                  <span className="text-[var(--leon-black)]/45"> &middot; originally {fmtDate(invoice.dueDateOriginal)}</span>}
              </p>
              <ForecastDateHistory history={invoice.dueDateHistory} label="Moved" />
            </div>
          )}
          {invoice.description && <p className="text-sm">{invoice.description}</p>}
          {invoice.file && <p className="text-sm"><FileField name={invoice.file} url={invoice.fileUrl} editable={false} onChange={() => {}} /></p>}
          {invoice.lines && invoice.lines.length > 0 && (
            <div>
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Line Items — {project ? project.name : ''}</p>
              <table className="w-full text-xs">
                <thead><tr className="text-left text-[var(--leon-black)]/40"><th className="py-1">Scope</th><th className="py-1">Description</th><th className="py-1">Amount</th></tr></thead>
                <tbody>{invoice.lines.map((l, i) => {
                  const lScope = project && project.scopes.find(s => s.id === l.scopeId);
                  return <tr key={i} className="border-t border-[var(--leon-line)]"><td className="py-1">{lScope ? lScope.name : '—'}</td><td className="py-1">{l.description || '—'}</td><td className="py-1 font-semibold">{fmtMoney(l.amount)}</td></tr>;
                })}</tbody>
              </table>
            </div>
          )}
          {groupMates.length > 0 && (
            <p className="text-xs text-[var(--leon-brown)] font-semibold">Part of a multi-project invoice — also billed under: {groupMates.map(g => `${g.projectName} (${fmtMoney(g.amount)})`).join(', ')}</p>
          )}

          <div className="grid grid-cols-2 gap-3">
            <div className="border border-[var(--leon-line)] rounded-lg p-3">
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Approval Status</p>
              <StatusBadge status={invoice.approvalStatus} />
              {invoice.approvedBy && <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">By {invoice.approvedBy}, {fmtDate(invoice.approvalDate)}</p>}
              {invoice.rejectionReason && <p className="text-[11px] text-[var(--leon-red)] mt-1">{invoice.rejectionReason}</p>}
              {canApprove && (
                <div className="flex gap-1.5 mt-2 flex-wrap">
                  <Button size="sm" onClick={() => ctx.setApInvoiceApproval(invoice.projectId, invoice.id, 'Approved')}>Approve</Button>
                  <Button size="sm" variant="ghost" onClick={() => setRejectModal('Revision Requested')}>Request Revision</Button>
                  <Button size="sm" variant="danger" onClick={() => setRejectModal('Rejected')}>Reject</Button>
                </div>
              )}
            </div>
            <div className="border border-[var(--leon-line)] rounded-lg p-3">
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Payment Status</p>
              <StatusBadge status={invoice.paymentStatus} />
              <p className="text-xs mt-1">Amount: {fmtMoney(invoice.amount)} · Paid: {fmtMoney(invoiceTotalPaid(invoice))} · <strong>Open: {fmtMoney(openBalance)}</strong></p>
              {canPay && <Button size="sm" className="mt-2" onClick={() => setPayModal(true)}>Record Payment</Button>}
              {!canApprove && invoice.approvalStatus !== 'Approved' && invoice.paymentStatus === 'Unpaid' && ctx.canSeeFin && (
                <p className="text-[11px] text-[var(--leon-black)]/40 mt-2">Cannot be paid until approved.</p>
              )}
              {ctx.canSeeFin && (
                <Select value={invoice.paymentStatus} onChange={e => ctx.setApInvoicePaymentStatus(invoice.projectId, invoice.id, e.target.value)} className="!mt-2 !py-1 !text-xs !w-40">
                  {AP_PAYMENT_STATUSES.map(s => <option key={s}>{s}</option>)}
                </Select>
              )}
            </div>
          </div>

          {invoice.payments.length > 0 && (
            <div>
              <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Payment History</p>
              <table className="w-full text-xs">
                <thead><tr className="text-left text-[var(--leon-black)]/40"><th className="py-1">Date</th><th className="py-1">Amount</th><th className="py-1">Method</th><th className="py-1">Reference</th><th className="py-1">Entered By</th></tr></thead>
                <tbody>{invoice.payments.map(p => <tr key={p.id} className="border-t border-[var(--leon-line)]"><td className="py-1">{fmtDate(p.date)}</td><td className="py-1">{fmtMoney(p.amount)}</td><td className="py-1">{p.method}</td><td className="py-1">{p.reference}</td><td className="py-1">{p.enteredBy}</td></tr>)}</tbody>
              </table>
            </div>
          )}

          <div>
            <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Audit Trail</p>
            <div className="space-y-1 max-h-40 overflow-y-auto">
              {invoice.history.map(h => (
                <p key={h.id} className="text-[11px] text-[var(--leon-black)]/50">{fmtDate(h.date)} {h.time} — <strong>{h.user}</strong>: {h.action}</p>
              ))}
            </div>
          </div>
        </div>
      </Modal>
      {/* Print output must live outside the Modal's own .no-print wrapper —
          see PackingListPrintModal for the same fix and why it's needed. */}
      {!!invoice && !payModal && !rejectModal && (
        <div className="print-only print-area p-8">
          <PrintDocHeader title={`Invoice #${invoice.invoiceNumber}`} meta={`${project ? project.name : ''} ${scope ? '· ' + scope.name : ''}`} />
          <table className="w-full text-sm mb-4">
            <tbody>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold w-1/3">Vendor / Subcontractor</td><td className="py-1">{invoice.vendorName}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">PO / PI / Work Order Ref</td><td className="py-1">{invoice.poReference || '—'}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Invoice / Due Date</td><td className="py-1">{fmtDate(invoice.invoiceDate)} / {fmtDate(invoice.dueDate)}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Approval Status</td><td className="py-1">{invoice.approvalStatus}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Payment Status</td><td className="py-1">{invoice.paymentStatus}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Amount</td><td className="py-1">{fmtMoney(invoice.amount)}</td></tr>
              <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Paid / Open Balance</td><td className="py-1">{fmtMoney(invoiceTotalPaid(invoice))} / {fmtMoney(openBalance)}</td></tr>
              {invoice.description && <tr className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold">Description</td><td className="py-1">{invoice.description}</td></tr>}
            </tbody>
          </table>
          <p className="font-bold text-sm mb-1">Audit Trail</p>
          <table className="w-full text-xs">
            <tbody>
              {invoice.history.map(h => (
                <tr key={h.id} className="border-b border-gray-200"><td className="py-1 pr-2 whitespace-nowrap">{fmtDate(h.date)} {h.time}</td><td className="py-1 pr-2 whitespace-nowrap">{h.user}</td><td className="py-1">{h.action}</td></tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <RecordPaymentModal open={payModal} onClose={() => setPayModal(false)} ctx={ctx} invoice={invoice} projectId={invoice.projectId} />
      <RejectInvoiceModal open={!!rejectModal} onClose={() => setRejectModal(null)} ctx={ctx} invoice={invoice} projectId={invoice.projectId} targetStatus={rejectModal} />
    </>
  );
}

function UsersView({ ctx, embedded }) {
  const [tab, setTab] = useState('users');
  return (
    <div>
      <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)]">
        {[{ key: 'users', label: 'Users' }, { key: 'rolePermissions', label: 'Role Permissions' }, { key: 'signIns', label: 'Sign-in Log' }].map(t => (
          <button key={t.key} onClick={() => setTab(t.key)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 -mb-px transition ${tab === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}
            {t.label}
          </button>
        ))}
      </div>
      {tab === 'users' ? <UsersDirectoryTab ctx={ctx} />
        : tab === 'rolePermissions' ? <RolePermissionsTab ctx={ctx} />
        : <SignInLogTab ctx={ctx} />}
    </div>
  );
}

// Who signed in and when — and, just as usefully, who failed to. A run of
// failures against one username is the only signal this app can give that
// someone is trying an account that is not theirs.
function SignInLogTab({ ctx }) {
  const [who, setWho] = useState('all');
  const [outcome, setOutcome] = useState('all');
  const [q, setQ] = useState('');
  const log = ctx.loginLog || [];
  const ql = q.trim().toLowerCase();
  const rows = log.filter(e =>
    (who === 'all' || e.userId === who) &&
    (outcome === 'all' || e.outcome === outcome) &&
    (!ql || [e.name, e.username, e.role].some(v => (v || '').toLowerCase().includes(ql)))
  );
  const failures = log.filter(e => e.outcome === 'Failed');
  // Last successful sign-in per person, so "who has never logged in" is
  // answerable — a live account nobody uses is worth knowing about.
  const lastByUser = {};
  log.forEach(e => { if (e.outcome === 'Signed in' && e.userId && !lastByUser[e.userId]) lastByUser[e.userId] = e; });
  const never = ctx.teamDirectory.filter(p => p.active && !lastByUser[p.id]);

  function exportCsv() {
    downloadCsv(`sign-in-log-${todayISO()}`, [
      { key: 'at', label: 'When' }, { key: 'name', label: 'Person' }, { key: 'username', label: 'Username' },
      { key: 'role', label: 'Role' }, { key: 'outcome', label: 'Outcome' }, { key: 'agent', label: 'Browser' },
    ], rows);
  }

  return (
    <div>
      <div className="flex items-start justify-between gap-3 flex-wrap mb-4">
        <div>
          <h1 className="text-2xl font-bold">Sign-in Log</h1>
          <p className="text-sm text-[var(--leon-black)]/50 max-w-2xl">
            Every sign-in, sign-out and failed attempt, newest first. The last {LOGIN_LOG_LIMIT} are kept.
          </p>
        </div>
        <Button variant="outline" onClick={exportCsv} disabled={!rows.length}>Export CSV</Button>
      </div>

      <div className="grid sm:grid-cols-3 gap-3 mb-4">
        <StatBox label="Events Recorded" value={String(log.length)} />
        <StatBox label="Failed Attempts" value={String(failures.length)} />
        <StatBox label="Active Accounts Never Used" value={String(never.length)} />
      </div>

      {never.length > 0 && (
        <Collapsible title="Active accounts that have never signed in" count={never.length}>
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">
            A live login nobody uses is worth reviewing &mdash; either the person needs help getting in,
            or the account should be deactivated.
          </p>
          <div className="flex flex-wrap gap-1.5">
            {never.map(p => (
              <span key={p.id} className="inline-flex items-center gap-1.5 px-2 py-1 rounded-lg border border-[var(--leon-line)] text-xs">
                <Avatar name={p.name} url={p.photoUrl} size={20} />
                {p.name}<span className="text-[var(--leon-black)]/40">{p.securityRole}</span>
              </span>
            ))}
          </div>
        </Collapsible>
      )}

      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search person, username or role…" className="!w-60 !py-1 !text-xs" />
        <Select value={who} onChange={e => setWho(e.target.value)} className="!w-52 !py-1 !text-xs">
          <option value="all">Everyone</option>
          {ctx.teamDirectory.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
        </Select>
        <Select value={outcome} onChange={e => setOutcome(e.target.value)} className="!w-40 !py-1 !text-xs">
          <option value="all">Any outcome</option>
          <option>Signed in</option><option>Signed out</option><option>Failed</option>
        </Select>
        <div className="flex-1" />
        <span className="text-xs text-[var(--leon-black)]/45">{rows.length} of {log.length}</span>
      </div>

      {rows.length === 0 ? <EmptyState text="Nothing recorded yet — the log starts from the next sign-in." /> : (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
          <table className="w-full text-xs" style={{ minWidth: 720 }}>
            <thead>
              <tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50 bg-[var(--leon-cream)]">
                <th className="px-3 py-2">When</th><th className="px-3 py-2">Person</th>
                <th className="px-3 py-2">Role</th><th className="px-3 py-2">Outcome</th><th className="px-3 py-2">Browser</th>
              </tr>
            </thead>
            <tbody>
              {rows.slice(0, 200).map(e => (
                <tr key={e.id} className={`border-t border-[var(--leon-line)] ${e.outcome === 'Failed' ? 'bg-[var(--leon-red)]/5' : ''}`}>
                  <td className="px-3 py-2 whitespace-nowrap">
                    {fmtDate(e.date)}
                    <span className="text-[var(--leon-black)]/45"> {new Date(e.at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</span>
                  </td>
                  <td className="px-3 py-2 font-semibold">
                    {e.name || <span className="text-[var(--leon-black)]/45 font-normal">tried &ldquo;{e.username}&rdquo;</span>}
                  </td>
                  <td className="px-3 py-2">{e.role || '—'}</td>
                  <td className="px-3 py-2">
                    {e.outcome === 'Failed' ? <Badge tone="red">Failed</Badge>
                      : e.outcome === 'Signed out' ? <Badge tone="neutral">Signed out</Badge>
                      : <Badge tone="green">Signed in</Badge>}
                  </td>
                  <td className="px-3 py-2 text-[var(--leon-black)]/40 max-w-[16rem] truncate" title={e.agent}>{e.agent || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-3">
        Prototype-only: the browser reports its own clock and its own identity, and none of this is
        verified on a server. Real sign-in auditing comes with real authentication &mdash; see
        docs/production-audit.
      </p>
    </div>
  );
}

// The per-role permission defaults, in one editable place — replacing what
// used to be ~23 hardcoded lists spread through data.jsx. Per-user overrides
// (Users tab -> Permissions) still win over anything set here.
function RolePermissionsTab({ ctx }) {
  const [role, setRole] = useState(SECURITY_ROLES[0]);
  // Every hook runs before any conditional return — the entry-missing guard
  // sits below them so hook order can never change between renders.
  const defaults = useMemo(() => buildDefaultRolePermissions()[role], [role]);
  const entry = ctx.rolePermissions[role];
  const isAdmin = role === 'Admin';
  const changedModules = entry ? ALL_MODULE_KEYS.filter(m => entry.modules[m.key] !== defaults.modules[m.key]).length : 0;
  const changedCaps = entry ? CAPABILITY_DEFS.filter(c => entry.capabilities[c.key] !== defaults.capabilities[c.key]).length : 0;
  const changed = changedModules + changedCaps;
  if (!entry) return <EmptyState text="No permission record for this role." />;

  const LEVELS = [
    { key: 'none', label: 'None', cls: 'text-[var(--leon-black)]/35' },
    { key: 'view', label: 'View', cls: 'text-[var(--leon-yellow)]' },
    { key: 'edit', label: 'Edit', cls: 'text-[var(--leon-green)]' },
  ];

  return (
    <div>
      <div className="flex items-start justify-between flex-wrap gap-3 mb-5">
        <div>
          <h1 className="text-2xl font-bold">Role Permissions</h1>
          <p className="text-sm text-[var(--leon-black)]/50 max-w-2xl">
            The default access every person with a given role gets. Changes apply immediately to everyone holding that role.
            Per-user overrides, set on the Users tab, still win over these defaults.
          </p>
        </div>
        <div className="flex items-center gap-2">
          {changed > 0 && <Badge tone="yellow">{changed} changed from default</Badge>}
          <Button size="sm" variant="ghost" onClick={() => { if (confirm(`Reset ${role} to its default permissions?`)) ctx.resetRolePermissions(role); }}>Reset this role</Button>
        </div>
      </div>

      <div className="flex flex-wrap gap-1 mb-5">
        {SECURITY_ROLES.map(r => {
          const e = ctx.rolePermissions[r];
          const n = e ? ALL_MODULE_KEYS.filter(m => e.modules[m.key] === 'edit').length : 0;
          return (
            <button key={r} onClick={() => setRole(r)}
              className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition ${role === r ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:border-[var(--leon-brown-light)]'}`}>
              {r} <span className="opacity-50">{n}</span>
            </button>
          );
        })}
      </div>

      {isAdmin && (
        <div className="mb-4 px-4 py-3 rounded-lg border border-[var(--leon-yellow)]/40 bg-[var(--leon-yellow)]/10 text-sm">
          <b>Admin is the recovery role.</b> Its access to the Users module is locked on, so a permission mistake made here can always be undone. Everything else is editable &mdash; but changes apply to every Admin immediately.
        </div>
      )}

      <div className="grid lg:grid-cols-2 gap-5">
        <div>
          <h2 className="text-sm font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">Module Access <span className="opacity-60">({ALL_MODULE_KEYS.length})</span></h2>
          <div className="bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden">
            <table className="w-full text-sm">
              <thead className="bg-[var(--leon-cream)]">
                <tr className="text-left text-xs uppercase text-[var(--leon-black)]/50">
                  <th className="px-4 py-2">Module</th><th className="px-4 py-2 w-52">Access</th>
                </tr>
              </thead>
              <tbody>
                {ALL_MODULE_KEYS.map(m => {
                  const level = entry.modules[m.key];
                  const isDefault = level === defaults.modules[m.key];
                  return (
                    <tr key={m.key} className="border-t border-[var(--leon-line)]">
                      <td className="px-4 py-2">
                        {m.label}
                        {!isDefault && <span className="ml-2 text-[10px] uppercase tracking-wide text-[var(--leon-yellow)] font-semibold">changed</span>}
                      </td>
                      <td className="px-4 py-2">
                        <div className="flex gap-1">
                          {LEVELS.map(l => {
                            const locked = isAdmin && m.key === 'users' && l.key !== 'edit';
                            return (
                              <button key={l.key} disabled={locked}
                                title={locked ? "Admin keeps Users access so a permission mistake here can always be undone" : ''}
                                onClick={() => ctx.setRoleModulePermission(role, m.key, l.key)}
                                className={`px-2.5 py-1 rounded text-[11px] font-semibold border transition ${level === l.key ? `border-[var(--leon-brown)] bg-[var(--leon-cream)] ${l.cls}` : 'border-[var(--leon-line)] text-[var(--leon-black)]/35 hover:border-[var(--leon-brown-light)]'} ${locked ? 'opacity-30 cursor-not-allowed hover:border-[var(--leon-line)]' : ''}`}>
                                {l.label}
                              </button>
                            );
                          })}
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>

        <div>
          <h2 className="text-sm font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">Actions &amp; Approvals <span className="opacity-60">({CAPABILITY_DEFS.length})</span></h2>
          <div className="bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden">
            {CAPABILITY_GROUPS.map(g => {
              const caps = CAPABILITY_DEFS.filter(c => c.group === g);
              if (!caps.length) return null;
              return (
                <div key={g}>
                  <div className="px-4 py-1.5 bg-[var(--leon-cream)] text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/50 border-t border-[var(--leon-line)]">{g}</div>
                  {caps.map(c => {
                    const on = entry.capabilities[c.key];
                    const isDefault = on === defaults.capabilities[c.key];
                    return (
                      <label key={c.key} className="flex items-center justify-between gap-3 px-4 py-2 border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]/50">
                        <span className="text-sm">
                          {c.label}
                          {!isDefault && <span className="ml-2 text-[10px] uppercase tracking-wide text-[var(--leon-yellow)] font-semibold">changed</span>}
                        </span>
                        <input type="checkbox" checked={on} onChange={e => ctx.setRoleCapability(role, c.key, e.target.checked)} className="w-4 h-4 accent-[var(--leon-brown)] shrink-0" />
                      </label>
                    );
                  })}
                </div>
              );
            })}
          </div>
        </div>
      </div>

      <PermissionLogPanel ctx={ctx} role={role} />

      <p className="text-[11px] text-[var(--leon-black)]/40 mt-4">
        Permissions are enforced in the browser only — a prototype gate, not real access control. See docs/production-audit for what server-side authorization requires.
      </p>
    </div>
  );
}

// THE TRAIL. Every other consequential edit in this app leaves one; permissions
// did not, and the gap only showed when an admin's assignments went missing and
// there was no way to tell whether they had been overwritten, never saved, or
// never made. It records the value BEFORE as well as after, so a change can be
// read back and reversed by hand.
function PermissionLogPanel({ ctx, role }) {
  const [all, setAll] = useState(false);
  const log = ctx.permissionLog || [];
  const rows = all ? log : log.filter(e => e.role === role);
  const label = k => {
    const m = ALL_MODULE_KEYS.find(x => x.key === k);
    if (m) return m.label;
    const c = CAPABILITY_DEFS.find(x => x.key === k);
    return c ? c.label : k;
  };
  return (
    <Collapsible id="perm-log" title="Change history" count={rows.length}>
      <div className="flex items-center justify-between gap-3 mb-2">
        <p className="text-[11px] text-[var(--leon-black)]/55">
          Who changed what, and what the value was before.
        </p>
        <label className="flex items-center gap-1.5 text-[11px] shrink-0">
          <input type="checkbox" checked={all} onChange={e => setAll(e.target.checked)} />
          Show every role
        </label>
      </div>
      {rows.length === 0 ? (
        <p className="text-xs text-[var(--leon-black)]/55">
          Nothing recorded {all ? 'yet' : `for ${role} yet`}. This log starts from the day it was added &mdash;
          changes made before that were never recorded anywhere, which is the reason it exists.
        </p>
      ) : (
        <div className="overflow-x-auto border border-[var(--leon-line)] rounded-lg bg-white">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left">
                <th className="px-2.5 py-1.5 font-semibold">When</th>
                <th className="px-2.5 py-1.5 font-semibold">Who</th>
                {all && <th className="px-2.5 py-1.5 font-semibold">Role</th>}
                <th className="px-2.5 py-1.5 font-semibold">What</th>
                <th className="px-2.5 py-1.5 font-semibold">From</th>
                <th className="px-2.5 py-1.5 font-semibold">To</th>
              </tr>
            </thead>
            <tbody>
              {rows.map(e => (
                <tr key={e.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-2.5 py-1.5 whitespace-nowrap">{String(e.date).slice(0, 16).replace('T', ' ')}</td>
                  <td className="px-2.5 py-1.5">{e.by}</td>
                  {all && <td className="px-2.5 py-1.5">{e.role}</td>}
                  <td className="px-2.5 py-1.5">{label(e.key)}</td>
                  <td className="px-2.5 py-1.5 text-[var(--leon-black)]/55">{String(e.from)}</td>
                  <td className="px-2.5 py-1.5 font-semibold">{String(e.to)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </Collapsible>
  );
}

// The directory was one very wide table with nine columns of live controls —
// 26 rows of it scrolled sideways and read as a wall. It is now a scannable
// list (who they are, what they do, what they can reach) with everything
// editable moved into a per-person editor. Nothing was removed; the edit
// controls just stopped competing with the ability to find someone.
function UsersDirectoryTab({ ctx }) {
  const [showNew, setShowNew] = useState(false);
  const [passwordFor, setPasswordFor] = useState(null);
  const [permissionsFor, setPermissionsFor] = useState(null);
  const [editing, setEditing] = useState(null);
  const [q, setQ] = useState('');
  const [fRole, setFRole] = useState('all');
  const [fDept, setFDept] = useState('all');
  const [fActive, setFActive] = useState('active');
  const [sortKey, setSortKey] = useState('name-asc');

  const ql = q.trim().toLowerCase();
  const shown = ctx.teamDirectory.filter(p =>
    (fRole === 'all' || p.securityRole === fRole) &&
    (fDept === 'all' || (p.departments || []).includes(fDept)) &&
    (fActive === 'all' || (fActive === 'active' ? p.active : !p.active)) &&
    (!ql || [p.name, p.title, p.email, p.username, p.securityRole].some(v => (v || '').toLowerCase().includes(ql)))
  );
  const sorted = [...shown].sort({
    'name-asc': (a, b) => textAsc(a.name, b.name),
    'name-desc': (a, b) => textDesc(a.name, b.name),
    'role-asc': (a, b) => textAsc(a.securityRole + a.name, b.securityRole + b.name),
  }[sortKey]);
  // Counts per role, so the filter itself tells you the shape of the company.
  const roleCounts = {};
  ctx.teamDirectory.forEach(p => { if (p.active) roleCounts[p.securityRole] = (roleCounts[p.securityRole] || 0) + 1; });

  return (
    <div>
      <div className="flex items-start justify-between gap-3 flex-wrap mb-4">
        <div>
          <h1 className="text-2xl font-bold">Users</h1>
          <p className="text-sm text-[var(--leon-black)]/50 max-w-2xl">
            Everyone with a login. Click a person to edit their role, departments, reporting line and access.
          </p>
        </div>
        <Button onClick={() => setShowNew(true)}>+ Add User</Button>
      </div>

      <div className="flex items-center gap-2 flex-wrap mb-3">
        <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search name, title, email…" className="!w-60 !py-1 !text-xs" />
        <Select value={fRole} onChange={e => setFRole(e.target.value)} className="!w-56 !py-1 !text-xs">
          <option value="all">All roles ({ctx.teamDirectory.filter(p => p.active).length})</option>
          {SECURITY_ROLES.filter(r => roleCounts[r]).map(r => <option key={r} value={r}>{r} ({roleCounts[r]})</option>)}
        </Select>
        <Select value={fDept} onChange={e => setFDept(e.target.value)} className="!w-40 !py-1 !text-xs">
          <option value="all">Both departments</option>
          {DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
        </Select>
        <Select value={fActive} onChange={e => setFActive(e.target.value)} className="!w-32 !py-1 !text-xs">
          <option value="active">Active only</option>
          <option value="inactive">Inactive only</option>
          <option value="all">All</option>
        </Select>
        <div className="flex-1" />
        <Select value={sortKey} onChange={e => setSortKey(e.target.value)} className="!w-40 !py-1 !text-xs">
          <option value="name-asc">Name (A–Z)</option>
          <option value="name-desc">Name (Z–A)</option>
          <option value="role-asc">Group by role</option>
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45">{sorted.length} of {ctx.teamDirectory.length}</span>
      </div>

      {sorted.length === 0 ? <EmptyState text="Nobody matches these filters." /> : (
        <div className="bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden">
          {sorted.map((p, i) => {
            const overrides = Object.keys(p.permissionOverrides || {}).length;
            const dual = (p.alsoReportsToIds || []).length;
            const lastLogin = (ctx.loginLog || []).find(e => e.userId === p.id && e.outcome === 'Signed in');
            return (
              <div key={p.id} className={`flex items-center gap-3 px-3 py-2.5 ${i ? 'border-t border-[var(--leon-line)]' : ''} ${p.active ? '' : 'opacity-55'} hover:bg-[var(--leon-cream)]/50 transition`}>
                <Avatar name={p.name} url={p.photoUrl} size={34} />
                <button onClick={() => setEditing(p)} className="min-w-0 flex-1 text-left">
                  <p className="text-sm font-bold leading-tight truncate">
                    {p.name}
                    {p.id === ctx.currentUserId && <span className="ml-1.5 text-[9px] uppercase tracking-wide text-[var(--leon-brown)] font-semibold">you</span>}
                  </p>
                  <p className={`text-xs leading-tight truncate ${p.title ? 'text-[var(--leon-black)]/55' : 'text-[var(--leon-black)]/30 italic'}`}>
                    {p.title || 'title not set'}
                  </p>
                  <p className="text-[11px] text-[var(--leon-black)]/40 truncate">{p.email}</p>
                </button>
                <div className="hidden md:flex flex-col items-end gap-1 shrink-0">
                  <span className="px-2 py-0.5 rounded text-[10px] font-semibold border border-[var(--leon-line)] text-[var(--leon-black)]/60 whitespace-nowrap">{p.securityRole}</span>
                  <span className="text-[10px] text-[var(--leon-black)]/40 whitespace-nowrap">{(p.departments || []).join(' + ') || 'no department'}</span>
                  {p.officeLocation && <span className="text-[10px] text-[var(--leon-black)]/45 whitespace-nowrap">{officeLabel(p.officeLocation)}</span>}
                </div>
                <div className="hidden lg:block w-40 shrink-0 text-right">
                  <p className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/35">Reports to</p>
                  <p className="text-xs truncate">{p.reportsToId ? personName(ctx.teamDirectory, p.reportsToId) : '— top of chart —'}</p>
                  {dual > 0 && <p className="text-[10px] text-[var(--leon-black)]/40 italic truncate">+{dual} dotted line</p>}
                </div>
                {/* Last successful sign-in — an active account nobody uses is
                    worth noticing from the list itself, not only in the log. */}
                <div className="hidden xl:block w-32 shrink-0 text-right">
                  <p className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/35">Last sign-in</p>
                  <p className={`text-xs ${lastLogin ? '' : 'text-[var(--leon-black)]/30 italic'}`}>
                    {lastLogin ? fmtDate(lastLogin.date) : 'never'}
                  </p>
                </div>
                <div className="flex items-center gap-1.5 shrink-0">
                  {overrides > 0 && <Badge tone="yellow">{overrides} override{overrides === 1 ? '' : 's'}</Badge>}
                  {!p.active && <Badge tone="neutral">Inactive</Badge>}
                  <Button size="sm" variant="ghost" onClick={() => setEditing(p)}>Edit</Button>
                </div>
              </div>
            );
          })}
        </div>
      )}

      <p className="text-[11px] text-[var(--leon-black)]/40 mt-3">Login is a prototype-only gate (checked in the browser, not on a server) — see docs/production-audit for what real authentication requires.</p>
      <AddUserModal open={showNew} onClose={() => setShowNew(false)} ctx={ctx} />
      <SetPasswordModal user={passwordFor} onClose={() => setPasswordFor(null)} ctx={ctx} />
      <UserPermissionsModal user={permissionsFor} onClose={() => setPermissionsFor(null)} ctx={ctx} />
      <EditUserModal
        user={editing ? ctx.teamDirectory.find(x => x.id === editing.id) : null}
        onClose={() => setEditing(null)} ctx={ctx}
        onPassword={u => { setEditing(null); setPasswordFor(u); }}
        onPermissions={u => { setEditing(null); setPermissionsFor(u); }} />
    </div>
  );
}

// Everything about one person, in one place. Every control writes straight
// through to ctx.updateUser as it is changed — there is no Save button because
// there is no draft; this is the same live-edit behaviour the old table row had.
function EditUserModal({ user, onClose, ctx, onPassword, onPermissions }) {
  if (!user) return null;
  const p = user;
  const overrides = Object.keys(p.permissionOverrides || {}).length;
  return (
    <Modal wide open={!!user} onClose={onClose} title={p.name}
      footer={<><Button variant="ghost" onClick={onClose}>Close</Button></>}>
      <div className="space-y-4">
        <div className="flex items-center gap-3">
          <Avatar name={p.name} url={p.photoUrl} size={48} />
          <div className="min-w-0">
            <p className="text-sm font-mono text-[var(--leon-black)]/55">{p.username}</p>
            <p className="text-xs text-[var(--leon-black)]/45 truncate">{p.email}{p.phone ? ` · ${p.phone}` : ''}</p>
          </div>
          <div className="flex-1" />
          {p.active ? <Badge tone="green">Active</Badge> : <Badge tone="neutral">Inactive</Badge>}
        </div>

        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Job title" hint="What they are called — shown on the org chart and in the header">
            <TextInput value={p.title || ''} onChange={e => ctx.updateUser(p.id, { title: e.target.value })} placeholder="e.g. Production Manager" />
          </Field>
          <Field label="Main office" hint="Where they work from — sets their time zone and the times on their calendar">
            <Select value={officeByKey(p.officeLocation) ? officeByKey(p.officeLocation).key : ''}
              onChange={e => ctx.updateUser(p.id, { officeLocation: e.target.value })}>
              <option value="">— not set —</option>
              {LEON_OFFICES.map(o => <option key={o.key} value={o.key}>{o.flag} {o.city}, {o.country}</option>)}
            </Select>
          </Field>
          <Field label="Security role" hint="Drives every permission">
            <Select value={p.securityRole} onChange={e => ctx.updateUser(p.id, { securityRole: e.target.value })}>
              {SECURITY_ROLES.map(r => <option key={r}>{r}</option>)}
            </Select>
          </Field>
        </div>

        {/* Department access — the data-scope axis. Deliberately separate from
            Role (the capability axis) so one role definition serves both
            departments. A person must keep at least one: un-ticking the last
            would lock them out of every project, so the toggle refuses it. */}
        <Field label="Departments"
          hint={departmentsLockedFor(p)
            ? `${p.securityRole} always covers both — the role runs the whole company.`
            : 'Which side of the business their data comes from'}>
          <div className="flex gap-1.5">
            {DEPARTMENTS.map(d => {
              // Admin, Accounting and the General Manager always cover both, so
              // the toggles show the truth and refuse to change it rather than
              // offering a setting that the app then ignores.
              const locked = departmentsLockedFor(p);
              const on = locked || (p.departments || []).includes(d);
              const isLast = locked || (on && (p.departments || []).length === 1);
              return (
                <button key={d} type="button" title={locked ? `${p.securityRole} always covers both departments` : (isLast ? 'A user must belong to at least one department' : `Toggle ${d} access`)}
                  onClick={() => { if (isLast) return; ctx.updateUser(p.id, { departments: on ? p.departments.filter(x => x !== d) : [...(p.departments || []), d] }); }}
                  className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition ${on ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/35'} ${isLast ? 'cursor-not-allowed' : ''}`}>
                  {d}
                </button>
              );
            })}
          </div>
        </Field>

        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Reports to" hint="Places their card on the org chart">
            <Select value={p.reportsToId || ''} onChange={e => ctx.updateUser(p.id, { reportsToId: e.target.value || null })}>
              <option value="">— top of chart —</option>
              {ctx.teamDirectory.filter(x => x.id !== p.id && x.active).map(x => <option key={x.id} value={x.id}>{x.name}</option>)}
            </Select>
          </Field>
          {/* Dual reporting. The primary above decides where the card sits;
              these show on it as dotted-line managers. */}
          <Field label="Also reports to" hint="Dotted-line managers, as many as apply">
            <div>
              <div className="flex flex-wrap gap-1 mb-1">
                {(p.alsoReportsToIds || []).map(id => (
                  <span key={id} className="inline-flex items-center gap-1 px-2 py-0.5 rounded border border-dashed border-[var(--leon-brown-light)] text-[11px] text-[var(--leon-brown)]">
                    {personName(ctx.teamDirectory, id)}
                    <button onClick={() => ctx.updateUser(p.id, { alsoReportsToIds: p.alsoReportsToIds.filter(x => x !== id) })} title="Remove">&#10005;</button>
                  </span>
                ))}
              </div>
              <Select value="" onChange={e => { if (e.target.value) ctx.updateUser(p.id, { alsoReportsToIds: [...new Set([...(p.alsoReportsToIds || []), e.target.value])] }); }}>
                <option value="">+ add a second manager…</option>
                {ctx.teamDirectory.filter(x => x.id !== p.id && x.active && x.id !== p.reportsToId && !(p.alsoReportsToIds || []).includes(x.id)).map(x => <option key={x.id} value={x.id}>{x.name}</option>)}
              </Select>
            </div>
          </Field>
        </div>

        <div className="flex items-center gap-2 flex-wrap pt-2 border-t border-[var(--leon-line)]">
          <Button size="sm" variant="outline" onClick={() => onPermissions(p)}>
            Permission overrides{overrides ? ` (${overrides})` : ''}
          </Button>
          <Button size="sm" variant="outline" onClick={() => onPassword(p)}>Reset password</Button>
          <div className="flex-1" />
          <Button size="sm" variant="ghost" onClick={() => ctx.setUserActive(p.id, !p.active)}>
            {p.active ? 'Deactivate account' : 'Reactivate account'}
          </Button>
        </div>
        {overrides > 0 && (
          <p className="text-[11px] text-[var(--leon-yellow)] font-semibold">
            This person has {overrides} per-user override{overrides === 1 ? '' : 's'} that win over their role&rsquo;s defaults.
          </p>
        )}
      </div>
    </Modal>
  );
}

function UserPermissionsModal({ user, onClose, ctx }) {
  if (!user) return null;
  const overrides = user.permissionOverrides || {};
  function setOverride(moduleKey, value) {
    const next = { ...overrides };
    if (value === 'default') delete next[moduleKey];
    else next[moduleKey] = value;
    ctx.updateUser(user.id, { permissionOverrides: next });
  }
  return (
    <Modal open={!!user} onClose={onClose} wide title={`Permissions — ${user.name}`} footer={<Button onClick={onClose}>Done</Button>}>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">Overrides win over this person's role default ({user.securityRole}) for that one module. Leave "Default" everywhere else.</p>
      <div className="border border-[var(--leon-line)] rounded-lg divide-y divide-[var(--leon-line)] max-h-[60vh] overflow-y-auto">
        {ALL_MODULE_KEYS.map(m => (
          <div key={m.key} className="flex items-center justify-between gap-3 px-3 py-2">
            <span className="text-sm">{m.label}</span>
            <Select value={overrides[m.key] || 'default'} onChange={e => setOverride(m.key, e.target.value)} className="!w-36 !py-1 !text-xs">
              <option value="default">Default</option>
              <option value="edit">Edit</option>
              <option value="view">View Only</option>
              <option value="none">Hidden</option>
            </Select>
          </div>
        ))}
      </div>
    </Modal>
  );
}
// fixedRole (Phase 8 — Driver Management) locks securityRole and hides the
// Role select, so this same modal covers the general "Add User" flow and
// the Logistics Hub's narrower "Add Driver" flow without duplicating it.
function AddUserModal({ open, onClose, ctx, fixedRole }) {
  const blank = { name: '', email: '', phone: '', username: '', password: '', securityRole: fixedRole || SECURITY_ROLES[0] };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, securityRole: fixedRole || SECURITY_ROLES[0] }); }, [open, fixedRole]);
  function submit() {
    if (!form.name.trim() || !form.username.trim() || !form.password) return;
    ctx.addUser({ ...form, username: form.username.trim().toLowerCase(), roles: [] });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={fixedRole ? 'Add Driver' : 'Add User'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{fixedRole ? 'Add Driver' : 'Add User'}</Button></>}>
      <div className="space-y-3">
        <Field label="Full Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
          <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Username" hint="Used to sign in."><TextInput value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} placeholder="e.g. jsmith" /></Field>
          <Field label="Password"><TextInput type="password" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} /></Field>
        </div>
        {!fixedRole && <Field label="Role"><Select value={form.securityRole} onChange={e => setForm({ ...form, securityRole: e.target.value })}>{SECURITY_ROLES.map(r => <option key={r}>{r}</option>)}</Select></Field>}
      </div>
    </Modal>
  );
}
// An administrator cannot set someone else's password from here, and this modal
// says so rather than offering a field that writes nothing.
//
// It used to write `user.password` on the person record. That was the whole
// login once, and it authenticates nothing now that sign-in is verified by a
// server — so an admin would set a password, hand it over, and the person could
// not get in with it. Doing it for real needs the service_role key, which cannot
// live in a page anyone can view-source.
//
// The two routes that DO work are both offered: a reset email the person
// follows themselves, and the Supabase dashboard.
function SetPasswordModal({ user, onClose, ctx }) {
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState('');
  useEffect(() => { setMsg(''); setBusy(false); }, [user]);
  async function sendReset() {
    if (!user) return;
    setBusy(true); setMsg('');
    try {
      const res = (typeof leonAuthSendPasswordReset === 'function')
        ? await leonAuthSendPasswordReset(user.email, window.location.origin)
        : { ok: false, error: 'Sign-in is unavailable — reload the page.' };
      setMsg(res.ok
        ? `Reset email sent to ${user.email}. They set their own password from the link; it expires in an hour.`
        : res.error);
    } finally { setBusy(false); }
  }
  return (
    <Modal open={!!user} onClose={onClose} title={user ? `Password — ${user.name}` : ''} footer={<Button onClick={onClose}>Close</Button>}>
      <div className="space-y-3 text-sm">
        <p className="text-[var(--leon-black)]/70">
          Passwords are held by the sign-in service, not by the Hub — nobody here, including an
          administrator, can read or set one. There are two ways to get {user ? user.name.split(' ')[0] : 'someone'} back in.
        </p>
        <div className="rounded-md border border-[var(--leon-line)] p-3">
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Send them a reset link</p>
          <p className="text-xs text-[var(--leon-black)]/60 mb-2">
            Goes to {user ? user.email : ''}. They choose their own password, so it is never handed
            over in a message. Needs mail to be configured for the project.
          </p>
          <div className="flex items-center gap-2 flex-wrap">
            <Button size="sm" variant="outline" onClick={sendReset} disabled={busy || !user || !user.email}>
              {busy ? 'Sending…' : 'Send reset email'}
            </Button>
            {msg && <span className="text-xs text-[var(--leon-black)]/70">{msg}</span>}
          </div>
        </div>
        <div className="rounded-md border border-[var(--leon-line)] p-3">
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Or set one directly</p>
          <p className="text-xs text-[var(--leon-black)]/60">
            Supabase dashboard → Authentication → Users → find {user ? user.email : 'the address'} →
            the ⋯ menu → Reset password. That is the only place a password can be set by hand,
            because it needs a key that must never be in this page.
          </p>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================================
// Calendar — tasks, stage due dates, deliveries, and export milestones
// ============================================================================
const CALENDAR_VIEWS = ['Day', 'Week', 'Month', 'Agenda'];
function eventTone(type) {
  return type === 'Task' ? 'brown' : type === 'Stage Due' ? 'yellow' : type === 'Delivery' ? 'green' : type === 'Jobsite Visit' ? 'blue' : type === 'Birthday' ? 'red' : 'neutral';
}
// Calendar events read as an undifferentiated list of text when every entry
// looks the same. Each type gets its own colour and a glyph, so a month view
// is scannable at a glance rather than something you have to read line by line.
const EVENT_STYLES = {
  // Deliberately six clearly different hues rather than shades of one — the
  // point of a month view is telling event KINDS apart at a glance.
  'Task':            { icon: '✅', bg: '#EDE9FE', bar: '#7C3AED', ink: '#4C1D95' },  // violet
  'Stage Due':       { icon: '🗓️', bg: '#FEF3C7', bar: '#D97706', ink: '#78350F' },  // amber
  'Delivery':        { icon: '🚚', bg: '#D1FAE5', bar: '#059669', ink: '#065F46' },  // emerald
  'Installation':    { icon: '🔧', bg: '#E0F2FE', bar: '#0284C7', ink: '#075985' },  // sky
  'Jobsite Visit':   { icon: '🚧', bg: '#FFEDD5', bar: '#EA580C', ink: '#7C2D12' },  // orange
  'Birthday':        { icon: '🎂', bg: '#FCE7F3', bar: '#DB2777', ink: '#9D174D' },  // pink
  // Red, and first in every day's list. A holiday is not a nice-to-know: it is
  // the reason a factory is shut and a sailing is missed, so it reads louder
  // than the work it will disrupt.
  'Holiday':         { icon: '🎌', bg: '#FEE2E2', bar: '#DC2626', ink: '#7F1D1D' },  // red
  // A month of reduced hours must not look like a closed factory, so the two
  // softer levels get their own colours rather than sharing the red.
  'Holiday-elevated':  { icon: '⚠️', bg: '#FFEDD5', bar: '#EA580C', ink: '#7C2D12' },  // orange
  'Holiday-awareness': { icon: '🕐', bg: '#FEF3C7', bar: '#D97706', ink: '#78350F' },  // amber
};
function eventStyle(type) {
  return EVENT_STYLES[type] || { icon: '•', bg: '#F4EFE8', bar: '#C3B5A2', ink: '#5C5145' };
}
function startOfWeek(dateStr) { return addDays(dateStr, -fromISO(dateStr).getDay()); }
function startOfMonthGrid(dateStr) {
  const d = fromISO(dateStr);
  const first = toISO(new Date(d.getFullYear(), d.getMonth(), 1));
  return startOfWeek(first);
}
function shiftCalendarDate(dateStr, view, dir) {
  if (view === 'Day') return addDays(dateStr, dir);
  if (view === 'Week') return addDays(dateStr, dir * 7);
  const d = fromISO(dateStr);
  return toISO(new Date(d.getFullYear(), d.getMonth() + dir, Math.min(d.getDate(), 28)));
}
function DayEventList({ events, onOpenProject, emptyText, clashes }) {
  if (events.length === 0) return <EmptyState text={emptyText || 'Nothing scheduled for this day.'} />;
  // Work scheduled inside a holiday window is the thing that quietly breaks a
  // ship date, so the warning goes ON the row rather than in a note beside it.
  const clashList = (clashes && clashes.length) ? clashes : null;
  return (
    <div className="space-y-1.5">
      {events.map((e, i) => {
        const st = eventStyle(e.styleKey || e.type);
        const clash = (e.type !== 'Holiday' && e.type !== 'Birthday') ? clashList : null;
        return (
          <div key={i} onClick={() => e.projectId && onOpenProject(e.projectId)}
            className={`flex items-center gap-3 rounded-lg pl-0 pr-3 py-2 bg-white border overflow-hidden transition ${clash ? 'border-[var(--leon-red)]/50' : 'border-[var(--leon-line)]'} ${e.projectId ? 'cursor-pointer hover:shadow-md hover:-translate-y-px' : ''}`}>
            {/* colour spine + glyph carries the type, so the row is readable
                without stopping to parse a badge */}
            <span className="self-stretch w-1.5 shrink-0" style={{ background: st.bar }} />
            <span className="text-base shrink-0" aria-hidden="true">{st.icon}</span>
            <div className="min-w-0 flex-1">
              <p className="text-sm font-semibold truncate">{e.label}</p>
              <p className="text-xs text-[var(--leon-black)]/50">{e.sub || e.project || e.type}</p>
              {clash && (
                <p className="text-[11px] font-semibold text-[var(--leon-red)] mt-0.5">
                  ⚠ Falls in a shutdown — {clash.map(c => `${c.name} (${c.country})`).join(', ')}
                </p>
              )}
            </div>
            <span className="text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full shrink-0"
                  style={{ background: st.bg, color: st.ink }}>{e.type}</span>
          </div>
        );
      })}
    </div>
  );
}
// Calendar Hub (Phase 8) — absorbs the old standalone Workload and
// Production Timeline nav items as subtabs alongside a restructured My
// To-Do/Calendar area, per the roadmap. Each subtab reuses an existing,
// unmodified component (MyToDoView/WorkloadView/ProductionTimelineView) or
// this same CalendarView filtered by event type — no new engines built.
const CALENDAR_HUB_SUBTABS = [
  { key: 'myToDo', label: 'My To-Do', icon: '✅' },
  // Every open quotation, chased weekly until it converts or dies.
  { key: 'quoteChase', label: 'Quote Follow-Ups', icon: '📨' },
  { key: 'calendar', label: 'Calendar', icon: '📅' },
  { key: 'workload', label: 'Workload', icon: '📊' },
  { key: 'production', label: 'Production', icon: '🏭' },
  { key: 'export', label: 'Export', icon: '🚢' },
  { key: 'deliveryCalendar', label: 'Delivery Calendar', icon: '🚚' },
  { key: 'installationCalendar', label: 'Installation & Punch List Calendar', icon: '🔧' },
];
function CalendarHubView({ ctx }) {
  // My To-Do is the landing tab; a refresh restores whichever tab was open.
  const [sub, setSub] = useHubSection(ctx, 'calendar', ctx.navMemo.calendarSub || 'myToDo');
  useEffect(() => { ctx.rememberNav({ calendarSub: sub }); }, [sub]);
  const tabs = CALENDAR_HUB_SUBTABS.filter(t => {
    if (t.key === 'workload') return ctx.canSeeWorkload;
    if (t.key === 'production') return ctx.canSeeProductionTimeline;
    if (t.key === 'export') return ctx.canSeeLogistics;
    // Chasing quotes is sales work, and it is only useful to someone who can
    // see the quotes in the first place.
    if (t.key === 'quoteChase') return ctx.canView('quotes');
    return true;
  });
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />
      {sub === 'myToDo' && <MyToDoView ctx={ctx} />}
      {sub === 'quoteChase' && (ctx.canView('quotes') ? <QuoteChaseView ctx={ctx} /> : <LockedNotice />)}
      {sub === 'calendar' && <CalendarView ctx={ctx} />}
      {sub === 'workload' && (ctx.canSeeWorkload ? <WorkloadView ctx={ctx} /> : <LockedNotice />)}
      {sub === 'production' && (ctx.canSeeProductionTimeline ? <ProductionTimelineView ctx={ctx} /> : <LockedNotice />)}
      {sub === 'export' && (ctx.canSeeLogistics ? <LogisticsExportShipmentsSubTab ctx={ctx} /> : <LockedNotice />)}
      {sub === 'deliveryCalendar' && <CalendarView ctx={ctx} typeFilter={['Delivery']} title="Delivery Calendar" subtitle="Every scheduled delivery across every project." />}
      {sub === 'installationCalendar' && <CalendarView ctx={ctx} typeFilter={['Installation']} title="Installation &amp; Punch List Calendar" subtitle="Every installation and punch-list return visit across every project." />}
    </div>
  );
}
// All shipments (export containers) across every project, sorted by ETD —
// the "Export" subtab of the Calendar Hub.
function LogisticsExportShipmentsSubTab({ ctx }) {
  const containers = containerShipmentRows(ctx.exportContainers, ctx.projects);
  const sorted = [...containers].sort((a, b) => ((a.etd || '9999') < (b.etd || '9999') ? -1 : 1));
  return (
    <div>
      <Collapsible title="All Shipments" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No export containers yet." /> : (
          <div className="space-y-1">
            {sorted.map(c => (
              <div key={c.id} className="flex items-center justify-between gap-3 py-2 border-b border-[var(--leon-line)] last:border-0 cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => ctx.goProjectTab(c.projectId, 'export')}>
                <div className="min-w-0">
                  <p className="text-sm font-semibold truncate">{c.containerNumber} <span className="font-normal text-[var(--leon-black)]/50">— {c.projectName}</span></p>
                  <p className="text-xs text-[var(--leon-black)]/50">{c.fromPort || '—'} → {c.toPort || '—'} · ETD {fmtDate(c.etd)} · ETA {fmtDate(c.eta)}</p>
                </div>
                <div className="flex items-center gap-2 shrink-0">
                  <StatusBadge status={c.status} />
                  <RiskBadge status={containerRiskStatus(c)} />
                </div>
              </div>
            ))}
          </div>
        )}
      </Collapsible>
    </div>
  );
}
// typeFilter (optional array of event `type` strings) — used by the
// Calendar Hub's Delivery Calendar / Installation & Punch List Calendar
// subtabs to reuse this exact component filtered down, rather than building
// a second calendar engine. title/subtitle override the default heading.
// Which countries' holidays to show. All of them by default — the point is
// that a shutdown elsewhere in the chain is exactly what you would otherwise
// miss — but a coordinator who never buys from Brazil can drop it.
function HolidayCountryFilter({ value, onChange }) {
  const [open, setOpen] = useState(false);
  const all = value.length === HOLIDAY_COUNTRIES.length;
  return (
    <span className="relative">
      <button type="button" onClick={() => setOpen(o => !o)}
        className="px-2 py-1 rounded-lg border border-[var(--leon-line)] bg-white text-xs font-semibold text-[var(--leon-black)]/60 hover:border-[var(--leon-brown-light)] whitespace-nowrap"
        title="Which countries' public holidays to show">
        🗓 Holidays{all ? '' : ` (${value.length})`}
      </button>
      {open && (
        <span className="absolute z-30 right-0 top-8 w-60 bg-white border border-[var(--leon-line)] rounded-lg shadow-lg p-2 block">
          <span className="flex items-center gap-2 pb-1 mb-1 border-b border-[var(--leon-line)]">
            <button onClick={() => onChange(HOLIDAY_COUNTRIES)} className="text-[11px] font-semibold text-[var(--leon-brown)]">All</button>
            <button onClick={() => onChange([])} className="text-[11px] font-semibold text-[var(--leon-black)]/45">None</button>
          </span>
          {HOLIDAY_COUNTRIES.map(c => (
            <label key={c} className="flex items-center gap-2 py-0.5 text-xs cursor-pointer">
              <input type="checkbox" checked={value.includes(c)} className="w-3.5 h-3.5 accent-[var(--leon-brown)]"
                onChange={e => onChange(e.target.checked ? [...value, c] : value.filter(x => x !== c))} />
              <span>{HOLIDAY_COUNTRY_FLAGS[c]} {c}</span>
            </label>
          ))}
        </span>
      )}
    </span>
  );
}

function CalendarView({ ctx, typeFilter, title, subtitle }) {
  const [selectedDate, setSelectedDate] = useState(todayISO());
  const [view, setView] = useState('Month');
  const [assigneeFilter, setAssigneeFilter] = useState('__all__');
  // Holidays are shown to everyone by default. A closed factory in Vietnam or
  // a shut port in China moves a date whether or not you work in that country,
  // so the filter starts with all six on rather than off.
  const [holidayCountries, setHolidayCountries] = useState(HOLIDAY_COUNTRIES);

  const allEvents = useMemo(() => {
    const list = [];
    ctx.projects.forEach(p => {
      p.tasks.forEach(t => { if (t.status !== 'Completed') list.push({ date: t.dueDate, type: 'Task', label: t.title, project: p.name, projectId: p.id, assigneeId: t.assigneeId || null }); });
      p.scopes.forEach(s => s.stages.forEach(st => { if (st.status !== 'Completed') list.push({ date: st.plannedDue, type: 'Stage Due', label: `${s.name} — ${st.name}`, project: p.name, projectId: p.id, assigneeId: st.assignedUserId || null }); }));
      p.deliveries.forEach(d => list.push({ date: d.date, type: 'Delivery', label: d.description, project: p.name, projectId: p.id, assigneeId: d.driverId || null }));
      (p.jobsiteVisits || []).forEach(v => list.push({ date: v.date, type: 'Jobsite Visit', label: v.purpose || 'Jobsite Visit', project: p.name, projectId: p.id, assigneeId: v.assigneeId || null }));
      (p.installationRecords || []).forEach(r => {
        if (r.scheduledStart) list.push({ date: r.scheduledStart, type: 'Installation', label: `${r.building || ''} ${r.room || ''}`.trim() || 'Installation', project: p.name, projectId: p.id, assigneeId: null });
        if (r.returnVisit && r.returnVisit.date) list.push({ date: r.returnVisit.date, type: 'Installation', label: `Return Visit — ${`${r.building || ''} ${r.room || ''}`.trim() || 'Installation'}`, project: p.name, projectId: p.id, assigneeId: null });
      });
      (p.punchItems || []).forEach(pi => { if (pi.scheduledReturnDate) list.push({ date: pi.scheduledReturnDate, type: 'Installation', label: `Punch Return — ${pi.item || 'Punch Item'}`, project: p.name, projectId: p.id, assigneeId: null }); });
    });
    // Birthdays — everyone's, every year, on everyone's Calendar (assigneeId
    // left null like Installation events above, so the "Assigned To" filter
    // never hides someone's birthday from anyone else's view). Generated a
    // year back through two years ahead so navigating the calendar around
    // year boundaries still shows them.
    const thisYear = fromISO(todayISO()).getFullYear();
    ctx.teamDirectory.forEach(person => {
      if (!person.active || !person.birthday) return;
      for (let year = thisYear - 1; year <= thisYear + 2; year++) {
        list.push({ date: birthdayInYear(person.birthday, year), type: 'Birthday', label: `🎂 ${person.name}'s Birthday`, project: '', projectId: null, assigneeId: null });
      }
    });
    // Holidays are real events, not a decoration on the grid. Being in the
    // list is what makes them show up in the day count, the agenda, and every
    // filter — so nobody can scroll past the reason their date will not hold.
    (ctx.holidays || []).filter(x => x.active !== false).forEach(x => {
      const end = x.endDate || x.date;
      for (let d = x.date; d <= end; d = addDays(d, 1)) {
        const lvl = x.level || 'holiday';
        list.push({
          date: d, type: 'Holiday', projectId: null, project: '', assigneeId: null,
          styleKey: lvl === 'holiday' ? 'Holiday' : `Holiday-${lvl}`,
          label: `${HOLIDAY_COUNTRY_FLAGS[x.country] || ''} ${x.name} — ${x.country}`.trim(),
          // Whether OUR office in that country is shut is the fact the reader
          // actually needs; the holiday existing is only the reason for it.
          sub: [
            x.officeClosed === false ? 'Our office is OPEN' : 'Our office is CLOSED',
            lvl === 'elevated' ? 'approvals and inspections will slip'
              : lvl === 'awareness' ? 'reduced hours, work continues' : null,
            x.approx ? 'projected — confirm locally' : null,
          ].filter(Boolean).join(' · '),
          holiday: x,
        });
      }
    });
    return typeFilter ? list.filter(e => typeFilter.includes(e.type)) : list;
  }, [ctx.projects, ctx.teamDirectory, ctx.holidays, typeFilter]);

  // Installations still carry no single assignee, so they always pass
  // through regardless of who's selected. Deliveries now carry the
  // scheduled driver as their assigneeId once approved (null until then),
  // so they narrow the same way Task/Stage Due events do.
  const events = useMemo(() => {
    const byAssignee = assigneeFilter === '__all__'
      ? allEvents
      : allEvents.filter(e => e.assigneeId === null || e.assigneeId === assigneeFilter);
    // The country filter applies to holidays only; everything else passes.
    const filtered = byAssignee.filter(e => e.type !== 'Holiday' || holidayCountries.includes(e.holiday.country));
    // Holidays sort to the top of their day — they are the thing that changes
    // whether the rest of that day's list is achievable at all.
    return filtered.sort((a, b) => (a.type === 'Holiday' ? -1 : 0) - (b.type === 'Holiday' ? -1 : 0));
  }, [allEvents, assigneeFilter, holidayCountries]);

  // Which shutdowns cover the day being shown — buffer days included, since
  // the ramp-down and ramp-up are exactly when a date silently slips.
  const holidayClashes = useMemo(() => (ctx.holidays || []).filter(x => {
    if (x.active === false || !holidayCountries.includes(x.country)) return false;
    const w = holidayWindow(x);
    return selectedDate >= w.from && selectedDate <= w.to;
  }), [ctx.holidays, selectedDate, holidayCountries]);

  const eventsByDate = useMemo(() => {
    const m = {};
    events.forEach(e => { (m[e.date] = m[e.date] || []).push(e); });
    return m;
  }, [events]);

  const dayEvents = (eventsByDate[selectedDate] || []).slice().sort((a, b) => a.type.localeCompare(b.type));
  const eventDates = new Set(events.map(e => e.date));

  function goToday() { setSelectedDate(todayISO()); }
  function shift(dir) { setSelectedDate(d => shiftCalendarDate(d, view, dir)); }

  return (
    <div>
      <h1 className="text-2xl font-bold mb-1">{title || 'Calendar'}</h1>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4">{subtitle || 'Tasks, stage due dates, deliveries, and installation dates across every project.'}</p>

      <div className="flex items-center justify-between gap-2 mb-4 flex-wrap">
        <div className="flex items-center gap-2 flex-wrap">
          <Button size="sm" variant="ghost" onClick={() => shift(-1)}>← Prev</Button>
          <TextInput type="date" value={selectedDate} onChange={e => setSelectedDate(e.target.value)} className="!w-44" />
          <Button size="sm" variant="ghost" onClick={() => shift(1)}>Next →</Button>
          <Button size="sm" variant="ghost" onClick={goToday}>Today</Button>
          <Select value={assigneeFilter} onChange={e => setAssigneeFilter(e.target.value)} className="!w-48 !py-1 !text-xs">
            <option value="__all__">Assigned To: Everyone</option>
            <option value={ctx.currentUserId}>Assigned To: Me ({ctx.currentUserName})</option>
            {ctx.teamDirectory.filter(p => p.id !== ctx.currentUserId && p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
          <HolidayCountryFilter value={holidayCountries} onChange={setHolidayCountries} />
        </div>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {CALENDAR_VIEWS.map(v => (
            <button
              key={v}
              onClick={() => setView(v)}
              className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors ${view === v ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}
            >
              {v}
            </button>
          ))}
        </div>
      </div>

      {view === 'Day' && (
        <>
          <div className="flex gap-1 mb-5 overflow-x-auto pb-1">
            {Array.from({ length: 14 }, (_, i) => addDays(selectedDate, i - 3)).map(d => (
              <button
                key={d}
                onClick={() => setSelectedDate(d)}
                className={`shrink-0 w-14 rounded-lg border px-1 py-2 text-center ${d === selectedDate ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'}`}
              >
                <div className="text-[10px] uppercase opacity-60">{fromISO(d).toLocaleDateString('en-US', { weekday: 'short' })}</div>
                <div className="text-sm font-bold">{fromISO(d).getDate()}</div>
                {eventDates.has(d) && <div className={`w-1 h-1 rounded-full mx-auto mt-1 ${d === selectedDate ? 'bg-white' : 'bg-[var(--leon-brown)]'}`} />}
              </button>
            ))}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayEvents.length} item{dayEvents.length === 1 ? '' : 's'}</h2>
          <DayEventList events={dayEvents} onOpenProject={ctx.goProject} clashes={holidayClashes} />
        </>
      )}

      {view === 'Week' && (
        <>
          <div className="grid grid-cols-7 gap-1.5 mb-5">
            {Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(selectedDate), i)).map(d => {
              const dEvents = eventsByDate[d] || [];
              const isToday = d === todayISO();
              return (
                <button
                  key={d}
                  onClick={() => setSelectedDate(d)}
                  className={`text-left rounded-lg border px-2 py-2 min-h-[110px] align-top ${d === selectedDate ? 'border-[var(--leon-black)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'}`}
                >
                  <div className="flex items-center justify-between">
                    <span className="text-[10px] uppercase opacity-60">{fromISO(d).toLocaleDateString('en-US', { weekday: 'short' })}</span>
                    <span className={`text-xs font-bold ${isToday ? 'text-[var(--leon-brown)]' : ''}`}>{fromISO(d).getDate()}</span>
                  </div>
                  <div className="mt-1.5 space-y-1">
                    {dEvents.slice(0, 4).map((e, i) => (
                      <div key={i} className="text-[10px] truncate px-1 py-0.5 rounded bg-[var(--leon-line)]">{e.label}</div>
                    ))}
                    {dEvents.length > 4 && <div className="text-[10px] text-[var(--leon-black)]/40">+{dEvents.length - 4} more</div>}
                  </div>
                </button>
              );
            })}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayEvents.length} item{dayEvents.length === 1 ? '' : 's'}</h2>
          <DayEventList events={dayEvents} onOpenProject={ctx.goProject} clashes={holidayClashes} />
        </>
      )}

      {view === 'Month' && (
        <>
          <h2 className="font-bold text-sm mb-2">{fromISO(selectedDate).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}</h2>
          <div className="grid grid-cols-7 gap-1.5 mb-5">
            {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => (
              <div key={d} className="text-[10px] uppercase font-semibold text-[var(--leon-black)]/40 text-center pb-1">{d}</div>
            ))}
            {Array.from({ length: 42 }, (_, i) => addDays(startOfMonthGrid(selectedDate), i)).map(d => {
              const dEvents = eventsByDate[d] || [];
              const inMonth = fromISO(d).getMonth() === fromISO(selectedDate).getMonth();
              const isToday = d === todayISO();
              return (
                <button
                  key={d}
                  onClick={() => setSelectedDate(d)}
                  className={`text-left rounded-lg border px-1.5 py-1.5 min-h-[80px] align-top ${d === selectedDate
                      ? 'border-[var(--leon-black)] bg-[var(--leon-cream)]'
                      : holidaysOn(ctx.holidays, d, holidayCountries).length
                        ? 'border-[var(--leon-yellow)]/50 bg-[var(--leon-yellow)]/[0.07] hover:bg-[var(--leon-yellow)]/15'
                        : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'} ${inMonth ? '' : 'opacity-35'}`}
                >
                  <span className={`text-xs font-bold inline-flex items-center justify-center ${isToday ? 'bg-[var(--leon-brown)] text-white rounded-full w-5 h-5' : ''}`}>{fromISO(d).getDate()}</span>
                  {/* A holiday is not an event ON the day, it is a fact ABOUT
                      the day — so it sits under the date as a band of flags
                      rather than queueing with the work. */}
                  {/* Coloured chips rather than identical grey dots — the mix
                      of colours tells you what KIND of day it is before you
                      click into it. */}
                  <div className="mt-1 space-y-0.5">
                    {dEvents.slice(0, 3).map((e, i) => {
                      const st = eventStyle(e.styleKey || e.type);
                      return (
                        <div key={i} className="flex items-center gap-1 rounded px-1 py-px overflow-hidden"
                             style={{ background: st.bg }} title={`${e.type}: ${e.label}`}>
                          <span className="w-1 h-2.5 rounded-sm shrink-0" style={{ background: st.bar }} />
                          <span className="text-[9px] leading-tight truncate" style={{ color: st.ink }}>{e.label}</span>
                        </div>
                      );
                    })}
                    {dEvents.length > 3 && (
                      <div className="flex items-center gap-0.5 pl-1">
                        {[...new Set(dEvents.slice(3).map(e => e.type))].slice(0, 4).map(t => (
                          <span key={t} className="w-1.5 h-1.5 rounded-full" style={{ background: eventStyle(t).bar }} />
                        ))}
                        <span className="text-[9px] text-[var(--leon-black)]/40 ml-0.5">+{dEvents.length - 3}</span>
                      </div>
                    )}
                  </div>
                </button>
              );
            })}
          </div>
          <h2 className="font-bold text-sm mb-2">{fmtDate(selectedDate)} — {dayEvents.length} item{dayEvents.length === 1 ? '' : 's'}</h2>
          <DayEventList events={dayEvents} onOpenProject={ctx.goProject} clashes={holidayClashes} />
        </>
      )}

      {view === 'Agenda' && (
        <div className="space-y-4">
          {Object.keys(eventsByDate).sort().filter(d => d >= todayISO()).length === 0 ? (
            <EmptyState text="No upcoming events." />
          ) : (
            Object.keys(eventsByDate).sort().filter(d => d >= todayISO()).slice(0, 60).map(d => (
              <div key={d}>
                <h3 className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">{fmtDate(d)}{d === todayISO() ? ' — Today' : ''}</h3>
                <DayEventList events={eventsByDate[d].slice().sort((a, b) => a.type.localeCompare(b.type))} onOpenProject={ctx.goProject} />
              </div>
            ))
          )}
        </div>
      )}
    </div>
  );
}

// ============================================================================
// Map — job locations
// ============================================================================
function MapView({ ctx }) {
  const withAddress = ctx.projects.filter(p => p.address);
  const withoutAddress = ctx.projects.filter(p => !p.address);
  const [filter, setFilter] = useState('All');
  const [selectedId, setSelectedId] = useState(null);

  const filtered = withAddress.filter(p => filter === 'All' || p.pipelineStatus === filter);
  const selected = filtered.find(p => p.id === selectedId) || filtered[0] || null;
  const account = selected ? ctx.accounts.find(a => a.id === selected.accountId) : null;
  const mapQuery = selected ? encodeURIComponent(selected.address) : '';

  return (
    <div>
      <h1 className="text-2xl font-bold mb-1">Map</h1>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4">Job locations across every project.{withoutAddress.length > 0 ? ` ${withoutAddress.length} project${withoutAddress.length === 1 ? '' : 's'} have no address on file.` : ''}</p>

      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)]">
        {['All', ...PIPELINE_STATUSES].map(s => (
          <button
            key={s}
            onClick={() => setFilter(s)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${filter === s ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}
          >
            {s} {s !== 'All' && <span className="text-[var(--leon-black)]/30">({withAddress.filter(p => p.pipelineStatus === s).length})</span>}
          </button>
        ))}
      </div>

      {filtered.length === 0 ? <EmptyState text="No projects with an address on file for this filter." /> : (
        <div className="grid md:grid-cols-[320px_1fr] gap-4">
          <div className="space-y-1.5 max-h-[640px] overflow-y-auto pr-1">
            {filtered.map(p => (
              <button
                key={p.id}
                onClick={() => setSelectedId(p.id)}
                className={`w-full text-left flex items-center gap-2.5 border rounded-lg px-3 py-2.5 ${(selected && selected.id === p.id) ? 'border-[var(--leon-black)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white hover:bg-[var(--leon-cream)]'}`}
              >
                <ProjectThumb project={p} size={36} />
                <div className="min-w-0">
                  <p className="text-sm font-semibold truncate">{p.name}</p>
                  <p className="text-xs text-[var(--leon-black)]/50 truncate">📍 {p.address}</p>
                </div>
              </button>
            ))}
          </div>

          <div className="border border-[var(--leon-line)] rounded-xl overflow-hidden bg-white">
            {selected ? (
              <>
                <div className="p-4 border-b border-[var(--leon-line)] flex items-center justify-between gap-3 flex-wrap">
                  <div className="min-w-0">
                    <div className="flex items-center gap-2 flex-wrap">
                      <span className="text-xs text-[var(--leon-black)]/40 font-medium">{selected.projectNumber}</span>
                      <StatusBadge status={selected.pipelineStatus} />
                    </div>
                    <p className="font-bold cursor-pointer hover:underline" onClick={() => ctx.goProject(selected.id)}>{selected.name}</p>
                    <p className="text-xs text-[var(--leon-black)]/50">{account ? account.name : '—'} · 📍 {selected.address}</p>
                  </div>
                  <div className="flex items-center gap-2 shrink-0">
                    <a href={`https://www.google.com/maps/search/?api=1&query=${mapQuery}`} target="_blank" rel="noopener noreferrer" className="text-xs font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">Open in Google Maps ↗</a>
                    <Button size="sm" variant="ghost" onClick={() => ctx.goProject(selected.id)}>View Project</Button>
                  </div>
                </div>
                <iframe
                  title={`Map — ${selected.name}`}
                  src={`https://www.google.com/maps?q=${mapQuery}&output=embed`}
                  className="w-full h-[480px] border-0"
                  loading="lazy"
                  referrerPolicy="no-referrer-when-downgrade"
                />
              </>
            ) : <EmptyState text="Select a project to view its location." />}
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================================
// Company Reports (§11) — Financial, Sales, Documents, Vendor. Accounting /
// Admin only. Rolls up the same underlying data already tracked per project
// rather than introducing a separate reporting system.
// ============================================================================
function ReportRow({ label, value, strong }) {
  return (
    <div className={`flex items-center justify-between border-t border-[var(--leon-line)] py-1.5 text-sm ${strong ? 'font-bold' : ''}`}>
      <span className={strong ? '' : 'text-[var(--leon-black)]/60'}>{label}</span>
      <span className="font-semibold">{value}</span>
    </div>
  );
}

// ============================================================================
// Project Detail
// ============================================================================
// Tab order follows the user's explicit sequence; Renders and Production
// (not named in that sequence but pre-existing features) are kept, placed
// next to the most closely related tab rather than removed.
// Reordered/relabeled into "Hubs" per the restructuring roadmap (Phase 2) —
// keys and modules are untouched (so permissions/deep-links keep working),
// only label text and array order changed. "Scopes & Schedule" stays for
// now — it still owns all stage start/complete/delay actions until Phase
// 3/4 (chronology + per-scope hub subtabs) actually redistributes that.
// "Material Journey" is retired — its content is fully covered by the
// rebuilt Procurement Hub (allocated material) and Delivery and Dispatch
// Hub once those land; MaterialJourneyTab itself is left in place,
// unlinked, rather than deleted mid-roadmap.
// `icon` is picked to read as the thing the tab is ABOUT (a truck for
// delivery, a ship for export) rather than as decoration — with 18 tabs in
// one scrolling bar, the glyph is what lets someone re-find a tab without
// reading every label. Emoji keeps this dependency-free, matching how IconBtn
// and the footer already do icons in this app.
// `label` is the full name — it goes on the printed letterhead and in the PDF.
// `short` is what the tab bar shows: eight of nineteen tabs said "Hub", which
// is a word carrying no information when almost half of them have it.
const PROJECT_TABS = [
  { key: 'overview', label: 'Project Information', short: 'Info', phase: 'setup', module: 'overview', icon: '📋' },
  { key: 'tasks', label: 'Tasks', phase: 'setup', module: 'tasks', icon: '✅' },
  { key: 'issues', label: 'Issues', phase: 'setup', module: 'issues', icon: '⚠️' },
  { key: 'drawingSets', label: 'Drawing Sets', short: 'Drawings', phase: 'sell', module: 'drawingSets', icon: '📐' },
  { key: 'takeOffs', label: 'Take-offs Hub', short: 'Take-offs', phase: 'sell', module: 'takeOffs', icon: '📏' },
  { key: 'renders', label: 'Renders', phase: 'sell', module: 'renders', icon: '🖼️' },
  { key: 'sales', label: 'Sales Hub', short: 'Sales', phase: 'sell', module: 'quotes', financial: true, icon: '🤝' },
  { key: 'scopes', label: 'Schedule by Stages', short: 'Schedule', phase: 'plan', module: 'scopes', icon: '🗓️' },
  { key: 'selections', label: 'Selection Hub', short: 'Selections', phase: 'plan', module: 'selections', icon: '🎨' },
  { key: 'documents', label: 'Shop Drawing Hub', short: 'Shop Drawings', phase: 'plan', module: 'documents', icon: '📄' },
  { key: 'procurement', label: 'Procurement Hub', short: 'Procurement', phase: 'make', module: 'procurement', icon: '🛒' },
  { key: 'production', label: 'Production Hub', short: 'Production', phase: 'make', module: 'production', icon: '🏭' },
  { key: 'export', label: 'Export Hub', short: 'Export', phase: 'deliver', module: 'export', icon: '🚢' },
  { key: 'delivery', label: 'Delivery and Dispatch Hub', short: 'Delivery', phase: 'deliver', module: 'delivery', icon: '🚚' },
  // Hidden entirely when every scope on the job is Supply Only — there is no
  // installation to run.
  { key: 'installation', label: 'Installation Hub', short: 'Installation', phase: 'deliver', module: 'installation', icon: '🔧', installOnly: true },
  { key: 'financials', label: 'Financial Hub', short: 'Financial', phase: 'close', module: 'financials', financial: true, icon: '💰' },
  { key: 'reports', label: 'Reports', phase: 'close', module: 'reports', icon: '📊' },
  // Closing a job is its own step, so it gets its own tab rather than being
  // buried at the bottom of the schedule.
  { key: 'closeout', label: 'Closeout', phase: 'close', module: 'closeout', icon: '🏁' },
  { key: 'changelog', label: 'Change Log', short: 'Log', phase: 'close', module: 'changelog', icon: '🕘' },
];

function ProjectDetail({ ctx, project, pendingNav }) {
  // A deep link (search result, report drill-down) wins; otherwise restore the
  // tab this browser tab was last on, falling back to Project Information.
  const [tab, setTab] = useState(
    // A ?quote= link is the strongest signal there is about where to land.
    ctx.bootQuoteId ? 'sales' : ((pendingNav && pendingNav.tab) || ctx.navMemo.projectTab || 'overview'));
  useEffect(() => { ctx.rememberNav({ projectTab: tab }); }, [tab]);
  const [showPhoto, setShowPhoto] = useState(false);
  // ProjectDetail stays mounted while browsing within the same project, so a
  // second deep-link (search result, report drill-down) targeting a tab/
  // subtab while already inside this project needs an effect, not just the
  // useState initializer above, which only runs once on mount.
  useEffect(() => { if (pendingNav && pendingNav.tab) setTab(pendingNav.tab); }, [pendingNav]);
  const account = ctx.accounts.find(a => a.id === project.accountId);
  const fileRef = useRef(null);

  function onPickImage(e) {
    const file = e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => ctx.setProjectImage(project.id, reader.result);
    reader.readAsDataURL(file);
  }

  // The Sales Hub is normally gated on financial access. The salesperson
  // ASSIGNED to this job reaches it regardless — see isProjectSalesPerson —
  // because they own the client relationship and have to release that job's
  // subcontractor invoices. Inside, they see only the Job Costs subtab.
  const isSalesOwner = isProjectSalesPerson(project, ctx.currentUserId);
  const installs = (project.scopes || []).some(sc => !scopeIsSupplyOnly(sc, ctx.scopeLibrary));
  const tabs = PROJECT_TABS.filter(t =>
    (t.key === 'sales' && isSalesOwner)
    || ((!t.financial || ctx.canSeeFin) && (t.key !== 'changelog' || ctx.canSeeChangeLog)
        && (!t.installOnly || installs || (project.scopes || []).length === 0)
        && ctx.canView(t.module)));
  useEffect(() => { if (!tabs.some(t => t.key === tab)) setTab(tabs[0]?.key || 'overview'); }, [ctx.currentUserId]);

  const tabLabel = (tabs.find(t => t.key === tab) || {}).label || '';
  // What goes on the right of the printed letterhead — the job, so a page
  // handed to a vendor or a client says which job it is about without anyone
  // having to write it on.
  const printLines = [
    project.projectNumber,
    account ? account.name : null,
    project.address || null,
    [project.pipelineStatus, project.department].filter(Boolean).join(' · '),
  ].filter(Boolean);
  return (
    /* data-print-region: PrintButton walks up to the nearest one, so the
       button in the header prints the whole job and the one on the tab body
       prints just that tab. Same for a scope and a section further down. */
    <div data-print-region>
      <div className="flex items-center gap-3 mb-3">
        <button onClick={ctx.goDashboard} className="no-print text-sm text-[var(--leon-brown)] font-semibold">← Back to Dashboard</button>
        <div className="flex-1" />
        {/* Three icons, one meaning each: print it, download it, send it.
            Expand/collapse moved down under the tabs, beside the screen it
            actually acts on. */}
        <DocActions title={project.name} heading={project.name} lines={printLines} />
        <span className="no-print"><ShareButton ctx={ctx} projectId={project.id} subjectKey={`project:${project.id}`}
          subject={project.name}
          summary={`${project.projectNumber} · ${project.pipelineStatus} · ${project.scopes.length} scope${project.scopes.length === 1 ? '' : 's'}`} /></span>
      </div>

      <div className="bg-white border border-[var(--leon-line)] rounded-xl p-4 mb-4 flex flex-wrap items-center gap-4">
        <div className="relative group">
          <ProjectThumb project={project} size={64} />
          <div className="no-print absolute inset-0 rounded-md bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center gap-1 transition-opacity">
            {project.displayImageUrl && <IconBtn title="View photo larger" className="text-white hover:bg-white/20" onClick={() => setShowPhoto(true)}>🔍</IconBtn>}
            <IconBtn title="Upload project photo" className="text-white hover:bg-white/20" onClick={() => fileRef.current.click()}>⬆</IconBtn>
            {project.displayImageUrl && <IconBtn title="Remove" className="text-white hover:bg-white/20" onClick={() => ctx.removeProjectImage(project.id)}>✕</IconBtn>}
          </div>
          <button
            type="button"
            title="Change project photo"
            onClick={() => fileRef.current.click()}
            className="no-print absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-[var(--leon-brown)] text-white text-xs flex items-center justify-center border-2 border-white shadow"
          >
            📷
          </button>
          <input ref={fileRef} type="file" accept="image/*" className="hidden" onChange={onPickImage} />
          {showPhoto && <AttachmentViewerModal name={`${project.name} — Project Photo`} url={project.displayImageUrl}
            onClose={() => setShowPhoto(false)} replaceLabel="Replace project photo"
            onReplace={dataUrl => ctx.setProjectImage(project.id, dataUrl)} />}
        </div>
        <div className="flex-1 min-w-0">
          <div className="flex items-center gap-2 flex-wrap">
            <span className="text-xs text-[var(--leon-black)]/40 font-medium">{project.projectNumber}</span>
            <StatusBadge status={project.pipelineStatus} />
            <ComplexityStars level={project.complexity} />
            <Badge tone="neutral">{project.companyDepartment.join(' + ')}</Badge>
            <Badge tone="neutral">{project.projectType}</Badge>
            <HealthDot level={project.health} showLabel />
          </div>
          <h1 className="text-xl font-bold">{project.name}</h1>
          {/* The client's name is a way TO the client, not a caption. Same for
              the address, which is a place you want on a map. */}
          <p className="text-sm text-[var(--leon-black)]/50">
            {account
              ? <button onClick={() => ctx.goAccountDetail(account.id)}
                  className="no-print font-semibold text-[var(--leon-brown)] hover:underline">{account.name}</button>
              : '—'}
            {' · '}{project.department}
            {project.address && <> · <a className="hover:underline" target="_blank" rel="noopener noreferrer"
              href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(project.address)}`}
              title="Open in Maps">{project.address}</a></>}
          </p>
        </div>
      </div>

      <FlowTabs tabs={tabs} active={tab} onChange={setTab} />
      <div className="pt-3 hub-wrap" data-print-region>
        {/* The fallback toolbar, for a tab that has no subtabs of its own.
            A tab WITH subtabs renders <HubTools/> under them, and the :has()
            rule in styles.css stands this one down — so there is only ever one
            toolbar on screen, and it is the most specific one. */}
        <div className="hub-tools-parent no-print flex items-center justify-end gap-1 mb-2">
          <ExpandCollapseAll />
          <span className="w-px h-5 bg-[var(--leon-line)] mx-1" />
          <DocActions title={`${project.name} — ${tabLabel}`} heading={tabLabel} lines={[project.name, ...printLines]} />
        </div>
        {tab === 'overview' && <OverviewTab ctx={ctx} project={project} />}
        {tab === 'drawingSets' && <DrawingSetsTab ctx={ctx} project={project} />}
        {tab === 'takeOffs' && <TakeOffsTab ctx={ctx} project={project} />}
        {tab === 'scopes' && <ScopesTab ctx={ctx} project={project} />}
        {tab === 'selections' && <SelectionsTab ctx={ctx} project={project} account={account} />}
        {tab === 'renders' && <RendersTab ctx={ctx} project={project} />}
        {tab === 'documents' && <DocumentsTab ctx={ctx} project={project} />}
        {tab === 'procurement' && <ProcurementTab ctx={ctx} project={project} />}
        {tab === 'production' && <ProductionTab ctx={ctx} project={project} />}
        {tab === 'delivery' && <DeliveryTab ctx={ctx} project={project} />}
        {tab === 'export' && <ExportTab ctx={ctx} project={project} />}
        {tab === 'installation' && <InstallationTab ctx={ctx} project={project} pendingNav={pendingNav} />}
        {tab === 'tasks' && <TasksTab ctx={ctx} project={project} />}
        {tab === 'issues' && <IssuesTab ctx={ctx} project={project} />}
        {/* Financial access OR being the salesperson assigned to this job —
            the latter gets only the Job Costs subtab, gated inside SalesTab. */}
        {tab === 'sales' && ((ctx.canSeeFin || isSalesOwner) ? <SalesTab ctx={ctx} project={project} pendingNav={pendingNav} /> : <LockedNotice />)}
        {tab === 'financials' && (ctx.canSeeFin ? <FinancialsHubTab ctx={ctx} project={project} pendingNav={pendingNav} /> : <LockedNotice />)}
        {tab === 'reports' && <ReportsHubTab ctx={ctx} initialFilters={{ project: project.id }} />}
        {tab === 'closeout' && <CloseoutTab ctx={ctx} project={project} />}
        {tab === 'changelog' && <ChangeLogTab ctx={ctx} project={project} />}
      </div>
    </div>
  );
}

// ---- Overview -----------------------------------------------------------
// Overview is a subtab container, same pattern as DELIVERY_SUBTABS/DeliveryTab
// — Contacts and Meetings are relocated here (reusing the existing
// ContactsTab/MeetingsTab components unchanged) instead of being separate
// top-level PROJECT_TABS entries.
const OVERVIEW_SUBTABS = [
  { key: 'info', label: 'Project Information', icon: '📋' },
  { key: 'team', label: 'Team Assignment', icon: '👥' },
  { key: 'contacts', label: 'Contacts', icon: '📇' },
  { key: 'meetings', label: 'Meetings', icon: '📅' },
  { key: 'jobsiteVisits', label: 'Jobsite Visits', icon: '🚧' },
  { key: 'scopeRollup', label: 'Scope Rollup', icon: '📦' },
];
// Who can see this job. Deliberately NOT buried in a settings screen: a job
// being restricted is something everyone working on it should be able to see at
// a glance, so the panel is at the top of the project's own information tab and
// says plainly what "private" does and does not cover.
// Sales tax for this job. It sits on the JOB rather than on each quote because
// it follows the jurisdiction the work is installed in — every quote, change
// order and invoice on the job answers to the same rate, and holding it in one
// place is what stops two of them disagreeing.
// The state sales-tax library. Editable, because a rate is a fact about the
// world that changes and the team should not need a developer to correct one.
function SalesTaxLibraryTab({ ctx }) {
  const [q, setQ] = useState('');
  const rows = (ctx.salesTaxRates || []).filter(r =>
    !q.trim() || (r.name + ' ' + r.code).toLowerCase().includes(q.trim().toLowerCase()));
  const edited = (ctx.salesTaxRates || []).filter(r => r.edited).length;
  return (
    <div className="space-y-3">
      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-4">
        <p className="text-sm">
          The average combined state + local rate for each state, used to suggest a rate when a job&rsquo;s
          address names a state. Snapshot of <strong>{fmtDate(US_SALES_TAX_AS_OF)}</strong>.
        </p>
        {/* The two things that make this a starting point rather than an answer.
            Both are said here once, plainly, rather than implied. */}
        <ul className="mt-2 text-xs text-[var(--leon-black)]/65 space-y-1 list-disc pl-5">
          <li>
            <strong>It is a state AVERAGE.</strong> A job is taxed by its own jurisdiction — Chicago is
            10.25% against Illinois&rsquo;s {(((ctx.salesTaxRates || []).find(r => r.code === 'IL') || {}).rate * 100 || 0).toFixed(2)}% average.
            Wherever a state has local option, confirm the rate for the address before quoting.
          </li>
          <li>
            <strong>Whether the contract is taxable at all is a separate question.</strong> In many states
            a contractor improving real property is the consumer of the materials: tax is paid at
            purchase and none is charged to the client on the contract. The Hub cannot decide that — it
            turns on the state, the contract and the work.
          </li>
        </ul>
      </div>

      <div className="flex flex-wrap items-center gap-2">
        <TextInput placeholder="Find a state…" value={q} onChange={e => setQ(e.target.value)} className="!w-56 !py-1.5 !text-sm" />
        <span className="text-xs text-[var(--leon-black)]/45">
          {rows.length} of {(ctx.salesTaxRates || []).length}{edited ? ` · ${edited} edited here` : ''}
        </span>
      </div>

      <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
        <table className="w-full text-sm">
          <thead className="bg-[var(--leon-cream)]">
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
              <th className="px-3 py-2">State</th><th className="px-3 py-2 w-16">Code</th>
              <th className="px-3 py-2 w-32">Rate</th><th className="px-3 py-2">How to read it</th>
              <th className="px-3 py-2 w-20"></th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => (
              <tr key={r.code} className="border-t border-[var(--leon-line)]">
                <td className="px-3 py-1.5 font-semibold">{r.name}</td>
                <td className="px-3 py-1.5 text-[var(--leon-black)]/50">{r.code}</td>
                <td className="px-3 py-1.5">
                  <QPct w="w-20" value={r.rate} onChange={v => ctx.canManageCollection && ctx.updateSalesTaxRate(r.code, { rate: v })} />
                </td>
                <td className="px-3 py-1.5 text-xs text-[var(--leon-black)]/60">
                  {r.noSalesTax
                    ? <span className="font-semibold">No sales tax in this state.</span>
                    : r.exact
                      ? <span>No local option &mdash; this <strong>is</strong> the rate.</span>
                      : <span className="text-[var(--leon-black)]/45">State average &mdash; confirm the local rate.</span>}
                  {r.verify && <span className="ml-1 text-[var(--leon-red)] font-semibold">Verify.</span>}
                  {r.note && <span className="block text-[var(--leon-black)]/45">{r.note}</span>}
                </td>
                <td className="px-3 py-1.5 text-right">
                  {r.edited && ctx.canManageCollection && (
                    <button className="text-[11px] font-semibold text-[var(--leon-brown)] hover:underline"
                      onClick={() => ctx.resetSalesTaxRate(r.code)}>Reset</button>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}
function ProjectTaxPanel({ ctx, project, mirroredFrom }) {
  // Sales sets it while quoting; Accounting needs it while billing, and is
  // usually who holds the certificate. Both may edit — it is ONE record, so
  // there is nothing to keep in step — and the mirror says where it is set so
  // nobody thinks these are two different figures.
  const canEdit = mirroredFrom ? (ctx.canSeeAccountingHub || ctx.canEditProjectInfo) : ctx.canEditProjectInfo;
  const incomplete = projectTaxExemptIncomplete(project);
  // An expired certificate exempts nothing, so it is called out separately from
  // a missing one — the fix is different in each case.
  const expired = !!(project.taxExempt && project.taxExemptExpiry && project.taxExemptExpiry < todayISO());
  // The state read off the job's own address, and the library's rate for it.
  // OFFERED, never applied silently: a state average is a starting point, and a
  // rate that changed itself because someone edited an address is exactly the
  // kind of quiet change nobody notices until a quote is out.
  // A state STAMPED by picking an address off the map is a fact; reading it back
  // out of free text is a guess that `stateFromAddress` deliberately refuses to
  // make when it is unsure. Prefer the stamped one.
  const st = project.addressState || stateFromAddress(project.address);
  const libRate = st ? (salesTaxRateFor(ctx.salesTaxRates, st.code) || st) : null;
  const suggestion = libRate && !project.taxExempt
    && Math.abs((Number(project.taxRatePct) || 0) - (Number(libRate.rate) || 0)) > 0.00005
    ? libRate : null;
  const set = f => ctx.updateProjectInfo(project.id, f);
  // Collapsed by default: it is set once and then read, so it should not take a
  // third of the screen every time the page is opened. The headline carries the
  // answer — the rate, or that the job is exempt — and anything WRONG (a missing
  // certificate, an expired one) is shown in the header too, so a problem is
  // never hidden behind a closed section.
  const headline = project.taxExempt
    ? 'Tax exempt'
    : `Sales tax ${(Number(project.taxRatePct) || 0) * 100
        ? ((Number(project.taxRatePct) || 0) * 100).toFixed(3).replace(/\.?0+$/, '') + '%'
        : 'not set'}`;
  return (
    <Collapsible id={`tax-${project.id}`} title={headline}
      right={
        <span className="flex items-center gap-2 text-xs">
          {incomplete && <Badge tone="red">certificate missing</Badge>}
          {!incomplete && expired && <Badge tone="red">certificate expired</Badge>}
          {suggestion && !incomplete && <Badge tone="yellow">{suggestion.name} rate available</Badge>}
        </span>
      }>
      {/* A sales tax rate and an exemption claim are exactly the fields nobody
          should change by tabbing through a page they opened to read. */}
      <EditLock canEdit={canEdit} hint="Locked — press Edit to change the tax treatment of this job.">
      <div className="flex flex-wrap items-center gap-3">
        {canEdit && (
          <label className="flex items-center gap-2 text-sm">
            <input type="checkbox" checked={!!project.taxExempt}
              onChange={e => set({ taxExempt: e.target.checked })} />
            This job is tax exempt
          </label>
        )}
      </div>

      <div className="grid sm:grid-cols-3 gap-3 mt-3">
        {!project.taxExempt && (
          <Field label="Sales tax rate" hint="Of the jurisdiction the job is installed in.">
            <QPct w="w-24" value={project.taxRatePct} disabled={!canEdit} onChange={v => canEdit && set({ taxRatePct: v })} />
          </Field>
        )}
        {project.taxExempt && (
          <Field label="Certificate no." hint="How the form is looked up.">
            <TextInput value={project.taxExemptRef || ''} disabled={!canEdit}
              placeholder="e.g. ST-4 / resale no."
              onChange={e => set({ taxExemptRef: e.target.value })} />
          </Field>
        )}
        {project.taxExempt && (
          <Field label="Valid until" hint="Blank if it does not expire.">
            <TextInput type="date" value={project.taxExemptExpiry || ''} disabled={!canEdit}
              onChange={e => set({ taxExemptExpiry: e.target.value || null })} />
          </Field>
        )}
        <Field label="Note" className={project.taxExempt ? '' : 'sm:col-span-2'}>
          <TextInput value={project.taxNote || ''} disabled={!canEdit}
            placeholder="e.g. resale certificate, out-of-state delivery"
            onChange={e => set({ taxNote: e.target.value })} />
        </Field>
      </div>

      {/* The rate this job's own address points at. One click to take it. */}
      {suggestion && canEdit && (
        <div className="mt-3 rounded-lg border border-[var(--leon-brown)]/40 bg-[var(--leon-cream)] px-3 py-2.5">
          <div className="flex flex-wrap items-center gap-2">
            <span aria-hidden="true">🏛️</span>
            <span className="text-sm">
              This address is in <strong>{suggestion.name}</strong>
              {suggestion.noSalesTax
                ? <> &mdash; <strong>no sales tax</strong>.</>
                : <> &mdash; {suggestion.exact ? 'the rate there is' : 'the state average is'}{' '}
                    <strong>{(suggestion.rate * 100).toFixed(2)}%</strong>.</>}
            </span>
            <div className="flex-1" />
            <Button size="sm" variant="outline"
              onClick={() => set({ taxRatePct: suggestion.rate })}>Use {(suggestion.rate * 100).toFixed(2)}%</Button>
          </div>
          {!suggestion.exact && !suggestion.noSalesTax && (
            <p className="mt-1.5 text-[11px] text-[var(--leon-black)]/55">
              A <strong>state average</strong>, not this address&rsquo;s rate &mdash; a city can be two points
              either side of it. Confirm the local rate before the quote goes out.
              {suggestion.verify && <span className="text-[var(--leon-red)]"> {suggestion.note}</span>}
            </p>
          )}
        </div>
      )}
      {!st && !project.taxExempt && String(project.address || '').trim() && (
        <p className="mt-2 text-[11px] text-[var(--leon-black)]/45">
          No US state recognised in this job&rsquo;s address, so no rate is suggested. Set it by hand.
        </p>
      )}

      {/* The form itself. A reference number is a claim about a piece of paper;
          an audit asks for the paper. FileField is the app's shared uploader, so
          this certificate is shareable and printable like every other
          attachment without any new plumbing. */}
      {project.taxExempt && (
        <div className={`mt-3 rounded-lg border px-3 py-2.5 ${incomplete
          ? 'border-[var(--leon-red)] bg-[#fbeded]' : 'border-[var(--leon-line)] bg-[var(--leon-cream)]/60'}`}>
          <div className="flex flex-wrap items-center gap-2">
            <span className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">
              Exemption certificate
            </span>
            <FileField
              name={project.taxExemptForm ? project.taxExemptForm.name : ''}
              url={project.taxExemptForm ? project.taxExemptForm.url : ''}
              editable={canEdit}
              placeholder="Not attached"
              projectId={project.id}
              label={`Tax exemption certificate — ${project.name}`}
              onChange={(name, url) => set({ taxExemptForm: name ? { name, url } : null })} />
            {project.taxExemptForm && canEdit && (
              <button type="button" className="text-[11px] font-semibold text-[var(--leon-red)] hover:underline"
                onClick={() => set({ taxExemptForm: null })}>Remove</button>
            )}
          </div>
          {incomplete ? (
            <p className="mt-1.5 text-xs text-[var(--leon-red)]">
              <strong>The certificate is not attached.</strong> A job billed exempt without the form on
              file is the company&rsquo;s exposure on an audit, not the client&rsquo;s &mdash; a reference
              number is a claim about the paper, not the paper.
            </p>
          ) : expired ? (
            <p className="mt-1.5 text-xs text-[var(--leon-red)]">
              <strong>This certificate expired {fmtDate(project.taxExemptExpiry)}.</strong> An expired
              form does not exempt the job.
            </p>
          ) : null}
        </div>
      )}
      </EditLock>
      <p className="mt-2 text-[11px] text-[var(--leon-black)]/50">
        Every quotation, change order and invoice on this job uses this rate unless one deliberately
        overrides it.{mirroredFrom ? ` Set under ${mirroredFrom} — this is the same record, not a copy.` : ''}
      </p>
    </Collapsible>
  );
}
function ProjectPrivacyPanel({ ctx, project }) {
  const canEdit = ctx.canEditProjectInfo;
  const listed = project.visibleToUserIds || [];
  // Portal logins (client, subcontractor, driver, QC) are not colleagues and
  // have their own scoped views, so they are not offered here.
  const staff = ctx.teamDirectory.filter(p => p.active && PORTAL_ROLES.indexOf(p.securityRole) < 0);
  const [open, setOpen] = useState(false);
  if (!project.isPrivate && !canEdit) return null;
  const toggle = id => {
    const next = listed.indexOf(id) >= 0 ? listed.filter(x => x !== id) : listed.concat([id]);
    ctx.setProjectPrivacy(project.id, true, next);
  };
  return (
    <div className={`rounded-xl border p-3 ${project.isPrivate ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white'}`}>
      <div className="flex flex-wrap items-center gap-2">
        <span aria-hidden="true">{project.isPrivate ? '\u{1F512}' : '\u{1F513}'}</span>
        <span className="text-sm font-bold">{project.isPrivate ? 'Private job' : 'Open to everyone'}</span>
        {project.isPrivate && (
          <Badge tone="neutral">{listed.length} {listed.length === 1 ? 'person' : 'people'} + Admins</Badge>
        )}
        <div className="flex-1" />
        {canEdit && (
          <>
            {project.isPrivate && (
              <Button size="sm" variant="ghost" onClick={() => setOpen(o => !o)}>{open ? 'Done' : 'Choose who'}</Button>
            )}
            <Button size="sm" variant={project.isPrivate ? 'ghost' : 'outline'}
              onClick={() => { ctx.setProjectPrivacy(project.id, !project.isPrivate); setOpen(!project.isPrivate); }}>
              {project.isPrivate ? 'Make open' : 'Make private'}
            </Button>
          </>
        )}
      </div>
      {project.isPrivate && (
        <p className="text-xs text-[var(--leon-black)]/55 mt-1.5">
          Hidden from every list, report, calendar, dashboard and search for anyone not named below.
          {' '}<strong>Admins keep access</strong> &mdash; without that, a job whose only named person
          leaves could never be reached again.
        </p>
      )}
      {project.isPrivate && open && canEdit && (
        <div className="mt-2 max-h-64 overflow-y-auto border border-[var(--leon-line)] rounded-lg bg-white divide-y divide-[var(--leon-line)]">
          {staff.map(p => (
            <label key={p.id} className="flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer hover:bg-[var(--leon-cream)]">
              <input type="checkbox" checked={listed.indexOf(p.id) >= 0} onChange={() => toggle(p.id)} />
              <Avatar name={p.name} url={p.photoUrl} size={22} />
              <span className="font-semibold">{p.name}</span>
              <span className="text-xs text-[var(--leon-black)]/45">{personTitle(p) || p.securityRole}</span>
            </label>
          ))}
        </div>
      )}
      {project.isPrivate && !open && listed.length > 0 && (
        <p className="text-xs text-[var(--leon-black)]/60 mt-1.5">
          {listed.map(id => personName(ctx.teamDirectory, id)).join(', ')}
        </p>
      )}
    </div>
  );
}
function OverviewTab({ ctx, project }) {
  const [sub, setSub] = useState('info');
  // contacts/meetings keep respecting the exact same per-role view gate
  // that used to filter them out of the top-level tab bar (app.jsx:7877).
  const subtabs = OVERVIEW_SUBTABS.filter(t => (t.key !== 'contacts' || ctx.canView('contacts')) && (t.key !== 'meetings' || ctx.canView('meetings')));
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {subtabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}
            {t.label}
          </button>
        ))}
      </div>
      <HubTools />
      {sub === 'info' && <ProjectInfoSubTab ctx={ctx} project={project} />}
      {sub === 'scopeRollup' && <ScopeRollupSubTab project={project} />}
      {sub === 'team' && <TeamAssignmentSubTab ctx={ctx} project={project} />}
      {sub === 'contacts' && <ContactsTab ctx={ctx} project={project} />}
      {sub === 'meetings' && <MeetingsTab ctx={ctx} project={project} />}
      {sub === 'jobsiteVisits' && <JobsiteVisitsTab ctx={ctx} project={project} />}
    </div>
  );
}
function ProjectInfoSubTab({ ctx, project }) {
  const [notes, setNotes] = useState(project.notes);
  const [showDelete, setShowDelete] = useState(false);
  const editable = ctx.canEdit('overview');
  // Project Information and Team Assignments are editable only by
  // Admin/Accounting/General Manager/Production Director, per explicit
  // instruction — narrower than, and independent of, the general "overview"
  // module right above, which still governs Notes.
  const infoEditable = ctx.canEditProjectInfo;
  const isAdmin = ctx.currentRole === 'Admin';
  const isAccounting = ctx.currentRole === 'Accounting';
  const account = ctx.accounts.find(a => a.id === project.accountId);
  return (
    <>
    <div className="grid lg:grid-cols-3 gap-4">
      <div className="lg:col-span-2 space-y-3">
        <ProjectPrivacyPanel ctx={ctx} project={project} />
        <Collapsible title="Project Information">
          <EditLock canEdit={infoEditable} hint="Locked — press Edit to change these.">
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Address">
              {infoEditable ? (
                /* Picking a suggestion stamps the STATE as well as the text.
                   `stateFromAddress` reads it back off free text and returns
                   null whenever it is unsure — a picked address removes the
                   guesswork, and the sales-tax suggestion is the thing that
                   benefits. */
                <AddressAutocomplete value={project.address || ''}
                  onChange={v => ctx.updateProjectInfo(project.id, { address: v })}
                  onPick={r => ctx.updateProjectInfo(project.id, Object.assign(
                    { address: r.line || r.label },
                    r.state ? { addressState: r.state } : {},
                    r.city ? { addressCity: r.city } : {},
                    r.postcode ? { addressPostcode: r.postcode } : {},
                    (r.lat != null && r.lon != null) ? { addressLat: r.lat, addressLon: r.lon } : {}))} />
              ) : <p className="text-sm font-semibold">{project.address || '—'}</p>}
            </Field>
            <Field label="Project Type">
              {infoEditable ? (
                <Select value={project.projectType} onChange={e => ctx.updateProjectInfo(project.id, { projectType: e.target.value })}>
                  {PROJECT_TYPES.map(t => <option key={t}>{t}</option>)}
                </Select>
              ) : <p className="text-sm font-semibold">{project.projectType}</p>}
            </Field>
            <Field label="Size (Sq Ft)">
              {infoEditable ? <TextInput type="number" defaultValue={project.sizeSqFt || ''} onBlur={e => ctx.updateProjectInfo(project.id, { sizeSqFt: e.target.value ? Number(e.target.value) : null })} /> : <p className="text-sm font-semibold">{project.sizeSqFt ? project.sizeSqFt.toLocaleString() : '—'}</p>}
            </Field>
            <Field label="Unit Quantity">
              {infoEditable ? <TextInput type="number" defaultValue={project.unitQuantity || ''} onBlur={e => ctx.updateProjectInfo(project.id, { unitQuantity: e.target.value ? Number(e.target.value) : null })} /> : <p className="text-sm font-semibold">{project.unitQuantity ?? '—'}</p>}
            </Field>
            <Field label="Building Stories">
              {infoEditable ? <TextInput type="number" defaultValue={project.buildingStories || ''} onBlur={e => ctx.updateProjectInfo(project.id, { buildingStories: e.target.value ? Number(e.target.value) : null })} /> : <p className="text-sm font-semibold">{project.buildingStories ?? '—'}</p>}
            </Field>
            <Field label="Labor Type">
              {infoEditable ? (
                <Select value={project.laborType} onChange={e => ctx.updateProjectInfo(project.id, { laborType: e.target.value })}>
                  {LABOR_TYPES.map(t => <option key={t}>{t}</option>)}
                </Select>
              ) : <p className="text-sm font-semibold">{project.laborType}</p>}
            </Field>
            <Field label="Company Department">
              {infoEditable ? (
                <div className="flex gap-1.5">
                  {COMPANY_DEPARTMENTS.map(d => (
                    <button key={d} type="button"
                      onClick={() => {
                        const has = project.companyDepartment.includes(d);
                        if (has && project.companyDepartment.length === 1) return;
                        ctx.updateProjectInfo(project.id, { companyDepartment: has ? project.companyDepartment.filter(x => x !== d) : [...project.companyDepartment, d] });
                      }}
                      className={`px-3 py-1.5 rounded-lg text-xs font-semibold border ${project.companyDepartment.includes(d) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60'}`}>
                      {d}
                    </button>
                  ))}
                </div>
              ) : <p className="text-sm font-semibold">{project.companyDepartment.join(' + ')}</p>}
            </Field>
            <Field label="Complexity">
              {infoEditable ? (
                <Select value={project.complexity} onChange={e => ctx.updateProjectInfo(project.id, { complexity: e.target.value })}>
                  {complexityOptions(project.complexity).map(c => <option key={c.id} value={c.name}>{c.name} ({c.multiplier}x)</option>)}
                </Select>
              ) : <p className="text-sm font-semibold">{project.complexity}</p>}
            </Field>
            <Field label="Job Status" hint={ctx.canChangePipelineStatus ? undefined : 'Only Admin and Accounting can change this'}>
              {ctx.canChangePipelineStatus ? (
                <Select value={project.pipelineStatus} onChange={e => ctx.updateProjectInfo(project.id, { pipelineStatus: e.target.value })}>
                  {PIPELINE_STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
                </Select>
              ) : <StatusBadge status={project.pipelineStatus} />}
            </Field>
            <Field label="Account" hint={isAdmin ? undefined : 'Only Admin can move a project to another account'}>
              {isAdmin ? (
                <Select value={project.accountId} onChange={e => ctx.moveProjectToAccount(project.id, e.target.value)}>
                  {ctx.accounts.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
                </Select>
              ) : <p className="text-sm font-semibold">{account ? account.name : '—'}</p>}
            </Field>
          </div>
          </EditLock>
        </Collapsible>

        <Collapsible title="Notes">
          <TextArea rows={4} value={notes} disabled={!editable} onChange={e => setNotes(e.target.value)} onBlur={() => ctx.updateNotes(project.id, notes)} placeholder="Additional notes…" />
        </Collapsible>

        {(isAdmin || isAccounting) && (
          <Collapsible title="Danger Zone">
            {!project.deleteRequest ? (
              <>
                <p className="text-xs text-[var(--leon-black)]/50 mb-2">Permanently deletes this project and everything in it — scopes, financials, deliveries, documents, all of it. Requires both Admin and Accounting to sign off before it actually happens.</p>
                {isAdmin ? (
                  <Button variant="danger" size="sm" onClick={() => setShowDelete(true)}>Delete Project</Button>
                ) : (
                  <p className="text-xs text-[var(--leon-black)]/40 italic">Only Admin can start a deletion request — you'll be asked to approve it here once one is started.</p>
                )}
              </>
            ) : (
              <div>
                <p className="text-sm font-semibold text-[var(--leon-red)]">Deletion requested by {project.deleteRequest.requestedBy}, {fmtDate(project.deleteRequest.requestedDate)}</p>
                <p className="text-xs text-[var(--leon-black)]/50 mb-2">Both approvals are required before this project is actually deleted.</p>
                <div className="flex items-center gap-1.5 mb-2">
                  <Badge tone={project.deleteRequest.adminApproved ? 'green' : 'neutral'}>Admin {project.deleteRequest.adminApproved ? '✓' : 'pending'}</Badge>
                  <Badge tone={project.deleteRequest.accountingApproved ? 'green' : 'neutral'}>Accounting {project.deleteRequest.accountingApproved ? '✓' : 'pending'}</Badge>
                </div>
                <div className="flex items-center gap-2">
                  {isAdmin && !project.deleteRequest.adminApproved && <Button variant="danger" size="sm" onClick={() => ctx.decideProjectDeletion(project.id, 'admin')}>Approve Deletion (Admin)</Button>}
                  {isAccounting && !project.deleteRequest.accountingApproved && <Button variant="danger" size="sm" onClick={() => ctx.decideProjectDeletion(project.id, 'accounting')}>Approve Deletion (Accounting)</Button>}
                  {isAdmin && <Button variant="ghost" size="sm" onClick={() => ctx.cancelProjectDeletion(project.id)}>Cancel Request</Button>}
                </div>
              </div>
            )}
          </Collapsible>
        )}
      </div>

      <div className="space-y-3">
        <Collapsible title="Health Indicator">
          <div className="flex items-center gap-2 mb-2"><HealthDot level={project.health} showLabel /></div>
          <p className="text-xs text-[var(--leon-black)]/50">Derived automatically from stage delays, overdue stage due-dates, overdue tasks, and open high-severity issues.</p>
        </Collapsible>
      </div>
    </div>
    <DeleteProjectModal open={showDelete} project={project} onClose={() => setShowDelete(false)} ctx={ctx} />
    </>
  );
}
// Type-to-confirm — starts the two-person deletion request (Admin's own
// approval is implied by this confirmation); the project isn't actually
// deleted until Accounting separately approves too (see decideProjectDeletion).
function DeleteProjectModal({ open, project, onClose, ctx }) {
  const [confirmText, setConfirmText] = useState('');
  useEffect(() => { if (open) setConfirmText(''); }, [open]);
  if (!project) return null;
  const matches = confirmText.trim() === project.name;
  function submit() {
    if (!matches) return;
    ctx.requestProjectDeletion(project.id);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Delete Project — ${project.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit} disabled={!matches}>Request Deletion</Button></>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-red)] font-semibold">This starts a request to permanently delete "{project.name}" and everything in it — scopes, financials, deliveries, documents, containers, everything. Accounting will also need to approve before it actually happens.</p>
        <Field label={`Type the project name to confirm — "${project.name}"`}>
          <TextInput value={confirmText} onChange={e => setConfirmText(e.target.value)} placeholder={project.name} />
        </Field>
      </div>
    </Modal>
  );
}
function ScopeRollupSubTab({ project }) {
  return (
    <Collapsible title="Scope Rollup" count={project.scopes.length}>
      {project.scopes.length === 0 ? <EmptyState text="No scopes yet — add one under Scopes & Schedule." /> : (
        <div className="space-y-2">
          {project.scopes.map(s => {
            const done = s.stages.filter(st => st.status === 'Completed').length;
            const current = s.stages.find(st => st.status === 'In Progress' || st.status === 'Delayed');
            const delay = scopeTotalDelayDays(s);
            return (
              <div key={s.id} className="flex items-center justify-between gap-3 border border-[var(--leon-line)] rounded-lg px-3 py-2">
                <div className="min-w-0">
                  <p className="text-sm font-semibold truncate">{s.name}</p>
                  <p className="text-xs text-[var(--leon-black)]/50">{s.familyName} · {done}/{s.stages.length} stages complete{current ? ` · now: ${current.name}` : ''}</p>
                </div>
                <div className="text-right shrink-0">
                  <p className="text-xs font-semibold">{fmtDate(scopeProjectedCompletion(s))}</p>
                  {delay > 0 && <p className="text-[11px] text-[var(--leon-red)] font-semibold">+{delay}d delay</p>}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </Collapsible>
  );
}
// A job is staffed once per department: the people running its window
// package are usually not the people running its interior finishes. Only the
// departments this project actually has work in are shown, and only those the
// viewer covers — so a single-department user sees one team, not two.
function TeamAssignmentSubTab({ ctx, project }) {
  const infoEditable = ctx.canEditProjectInfo;
  const [assigning, setAssigning] = useState(null); // department being assigned
  const projectDepts = projectDepartments(project, ctx.scopeLibrary);
  const shown = (projectDepts.length ? projectDepts : DEPARTMENTS)
    .filter(d => ctx.activeDepartment === ALL_DEPARTMENTS || d === ctx.activeDepartment);
  const totalStaffed = shown.reduce((n, d) => n + TEAM_ROLES.filter(r => ((project.teams || {})[d] || {})[r]).length, 0);

  return (
    <Collapsible title="Team Assignments" count={totalStaffed}>
      {shown.length === 0 ? <EmptyState text="This project has no work in your department." /> : (
        <>
          {infoEditable && (
            <div className="flex justify-end mb-3">
              {/* One button rather than a department picker in the page: which
                  department you're staffing is the modal's first question. */}
              <Button size="sm" onClick={() => setAssigning(shown.length === 1 ? shown[0] : '')}>Assign Team</Button>
            </div>
          )}
          {/* Both departments sit side by side so a mixed job reads as two
              teams at a glance, not one long list. */}
          <div className={`grid gap-5 ${shown.length > 1 ? 'lg:grid-cols-2' : ''}`}>
            {shown.map(dept => {
              const team = (project.teams && project.teams[dept]) || {};
              const staffed = TEAM_ROLES.filter(r => team[r]).length;
              return (
                <div key={dept} className="border border-[var(--leon-line)] rounded-xl p-3 bg-white">
                  <div className="flex items-center justify-between gap-2 mb-2 pb-1.5 border-b border-[var(--leon-line)]">
                    <span className="text-xs font-bold uppercase tracking-wide text-[var(--leon-brown)]">
                      {dept === 'Windows' ? '🪟' : '🏠'} {dept} Team
                    </span>
                    <div className="flex items-center gap-2">
                      <span className="text-[11px] text-[var(--leon-black)]/40">{staffed}/{TEAM_ROLES.length} assigned</span>
                      {infoEditable && <button onClick={() => setAssigning(dept)} className="text-xs text-[var(--leon-brown)] font-semibold">Edit</button>}
                    </div>
                  </div>
                  <div className="space-y-1">
                    {TEAM_ROLES.map(role => (
                      <div key={role} className="flex items-center justify-between gap-2 text-sm">
                        <span className="text-[var(--leon-black)]/55">{role}</span>
                        {team[role]
                          ? <span className="font-semibold">{personName(ctx.teamDirectory, team[role])}</span>
                          : <span className="text-[var(--leon-black)]/25 text-xs">Unassigned</span>}
                      </div>
                    ))}
                  </div>
                </div>
              );
            })}
          </div>
        </>
      )}
      <AssignTeamModal
        open={assigning !== null}
        onClose={() => setAssigning(null)}
        ctx={ctx}
        project={project}
        departments={shown}
        initialDepartment={assigning}
      />
    </Collapsible>
  );
}

// Two steps in one dialog: which department's team, then who fills each role.
// Only people who actually cover that department are offered — assigning
// someone who can't see the work would create a silent dead end.
function AssignTeamModal({ open, onClose, ctx, project, departments, initialDepartment }) {
  const [dept, setDept] = useState(initialDepartment || '');
  const [form, setForm] = useState({});
  useEffect(() => {
    if (!open) return;
    const d = initialDepartment || (departments.length === 1 ? departments[0] : '');
    setDept(d);
    setForm(d ? { ...((project.teams || {})[d] || {}) } : {});
  }, [open, initialDepartment, project.id]);

  function pickDept(d) {
    setDept(d);
    setForm({ ...((project.teams || {})[d] || {}) });
  }
  function submit() {
    if (!dept) return;
    ctx.assignTeam(project.id, dept, form);
    onClose();
  }
  const eligible = ctx.teamDirectory.filter(p => p.active !== false && personCoversDepartment(p, dept));
  const filled = TEAM_ROLES.filter(r => form[r]).length;

  return (
    <Modal open={open} onClose={onClose} wide title="Assign Team"
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={submit} disabled={!dept}>{dept ? `Save ${dept} Team` : 'Select a department'}</Button>
      </>}>
      <div className="space-y-4">
        <div>
          <span className="block text-xs font-semibold text-[var(--leon-black)]/60 uppercase tracking-wide mb-1.5">Department</span>
          <div className="flex gap-2 flex-wrap">
            {departments.map(d => (
              <button key={d} type="button" onClick={() => pickDept(d)}
                className={`px-4 py-2 rounded-lg text-sm font-semibold border transition ${dept === d ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:border-[var(--leon-brown-light)]'}`}>
                {d === 'Windows' ? '🪟' : '🏠'} {d}
              </button>
            ))}
          </div>
        </div>

        {!dept ? (
          <EmptyState text="Pick a department to staff its team." />
        ) : (
          <div>
            <div className="flex items-center justify-between mb-2">
              <span className="text-xs font-semibold text-[var(--leon-black)]/60 uppercase tracking-wide">{dept} Team &mdash; {filled}/{TEAM_ROLES.length} assigned</span>
              <button type="button" onClick={() => setForm({})} className="text-xs text-[var(--leon-brown)] font-semibold">Clear all</button>
            </div>
            {eligible.length === 0 ? (
              <EmptyState text={`Nobody is assigned to the ${dept} department yet — set departments on the Users page first.`} />
            ) : (
              <div className="grid sm:grid-cols-2 gap-3">
                {TEAM_ROLES.map(role => (
                  <Field key={role} label={role}>
                    <Select value={form[role] || ''} onChange={e => setForm({ ...form, [role]: e.target.value || null })}>
                      <option value="">Unassigned</option>
                      {eligible.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                  </Field>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ---- Contacts -------------------------------------------------------------
// Picker shown above each fixed contact role. Sources people from the
// project's Account contact log; "+ New" adds them to that log first, so the
// contact exists once and is reusable on every other project for the same
// client rather than being stranded on this one.
function FixedRoleContactPicker({ ctx, project, role, current }) {
  const jobAccount = ctx.accounts.find(a => a.id === project.accountId);
  const [adding, setAdding] = useState(false);
  const blank = { name: '', title: '', phone: '', mobile: '', email: '', notes: '', preferredContactMethod: '' };
  const [form, setForm] = useState(blank);
  // Which account a NEW contact is filed under. Defaults to the job's, because
  // that is the common case, but it is a choice: the architect belongs to the
  // design practice, not to the client.
  const [fileUnder, setFileUnder] = useState('');
  useEffect(() => { if (adding) setFileUnder(project.accountId || ''); }, [adding, project.accountId]);

  // EVERY contact the company knows, not just this job's client. A job's people
  // are rarely all from one account — the architect, the GC and the client are
  // three different firms — so a picker limited to the job's account offers the
  // wrong half of the list.
  const contacts = allContacts(ctx.accounts);
  const selected = contacts.find(x => x.id === current.accountContactId)
    || contacts.find(x => current.person && x.name === current.person);

  function pick(id) {
    if (!id) { ctx.setProjectContact(project.id, role, {}); return; }
    const x = contacts.find(k => k.id === id);
    if (!x) return;
    ctx.setProjectContact(project.id, role, {
      // The COMPANY is the account the person is filed under, which is the
      // whole point of picking across accounts: the architect's company is the
      // practice, not the client.
      company: x.accountName, person: x.name, title: x.title, phone: x.phone,
      mobile: x.mobile, email: x.email, preferredContactMethod: x.preferredContactMethod,
      notes: x.notes, accountContactId: x.id, contactAccountId: x.accountId,
    });
  }
  function createAndAssign() {
    if (!form.name.trim()) return;
    // Added to the ACCOUNT first (so it joins the shared list), then assigned
    // to this role. addAccountContact returns the new contact's id.
    const target = ctx.accounts.find(a => a.id === fileUnder) || jobAccount;
    if (!target) return;
    const newId = ctx.addAccountContact(target.id, { role, ...form });
    ctx.setProjectContact(project.id, role, {
      company: target.name, person: form.name, title: form.title, phone: form.phone,
      mobile: form.mobile, email: form.email, preferredContactMethod: form.preferredContactMethod,
      notes: form.notes, accountContactId: newId || null, contactAccountId: target.id,
    });
    setForm(blank); setAdding(false);
  }

  return (
    <div className="mb-3 pb-3 border-b border-[var(--leon-line)]">
      {!adding ? (
        <div className="flex items-end gap-2 flex-wrap">
          <Field label="Contact" hint="Everyone we know, whichever company they are with." className="flex-1 min-w-[260px]">
            <Select value={selected ? selected.id : ''} onChange={e => pick(e.target.value)}>
              <option value="">— not assigned —</option>
              {contacts.map(x => (
                <option key={x.id} value={x.id}>
                  {x.name}{x.title ? ` — ${x.title}` : ''}{x.accountName ? ` · ${x.accountName}` : ''}
                </option>
              ))}
            </Select>
          </Field>
          <Button size="sm" variant="ghost" onClick={() => setAdding(true)}>+ New Contact</Button>
        </div>
      ) : (
        <div className="space-y-2">
          <p className="text-xs text-[var(--leon-black)]/50">
            A new contact is filed on a company first, so the next job can use them too, then assigned
            here as {role}.
          </p>
          <Field label="File them under" hint="The company they work for — not necessarily this job's client.">
            <Select value={fileUnder} onChange={e => setFileUnder(e.target.value)}>
              {ctx.accounts.map(a => (
                <option key={a.id} value={a.id}>{a.name}{a.id === project.accountId ? ' — this job\u2019s client' : ''}</option>
              ))}
            </Select>
          </Field>
          <div className="grid sm:grid-cols-2 gap-2">
            <Field label="Name"><TextInput autoFocus value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
            <Field label="Title"><TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} /></Field>
            <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
            <Field label="Mobile"><TextInput value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} /></Field>
            <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
            <Field label="Preferred Contact Method">
              <Select value={form.preferredContactMethod} onChange={e => setForm({ ...form, preferredContactMethod: e.target.value })}>
                <option value="">— none set —</option>
                {PREFERRED_CONTACT_METHODS.map(m => <option key={m}>{m}</option>)}
              </Select>
            </Field>
          </div>
          <div className="flex gap-2 justify-end">
            <Button size="sm" variant="ghost" onClick={() => { setForm(blank); setAdding(false); }}>Cancel</Button>
            <Button size="sm" onClick={createAndAssign}>Save &amp; Assign</Button>
          </div>
        </div>
      )}
    </div>
  );
}

function ContactsTab({ ctx, project }) {
  const account = ctx.accounts.find(a => a.id === project.accountId);
  // Editable only by Admin/Accounting/General Manager/Production Director,
  // per explicit instruction — replaces the general "contacts" module right.
  const editable = ctx.canEditProjectInfo;
  // Estimator first: during the bid phase they are the person the quote goes to
  // and the person it is chased with, so they should not be buried below the
  // roles that only start to matter once the job is won.
  const roles = ['Estimator', 'General Contractor', 'Owner', 'Developer', 'Architect', 'Designer', 'Jobsite Delivery Contact', 'Billing Contact'];
  const [showAdd, setShowAdd] = useState(false);
  return (
    <div>
      {roles.map(role => {
        const c = project.contacts[role] || { company: '', person: '', title: '', phone: '', mobile: '', email: '', preferredContactMethod: '', notes: '' };
        return (
          <Collapsible key={role} title={role} right={c.person ? <span className="text-xs text-[var(--leon-black)]/50">{c.person}</span> : <span className="text-xs text-[var(--leon-black)]/25">Unassigned</span>}>
            <EditLock canEdit={editable} hint="Locked — press Edit to change this contact.">
            {/* Fixed roles are filled FROM the account's shared contact list
                rather than retyped per project, so the same person isn't
                entered five different ways across five jobs. The fields below
                stay editable for per-project overrides. */}
            {editable && <FixedRoleContactPicker ctx={ctx} project={project} role={role} current={c} />}
            {/* Ways to ACT on the contact, not just read them. There is no
                separate contact profile in the Hub — the shared record lives on
                the account — so the link goes where the record actually is. */}
            {(c.person || c.email || c.phone) && (
              <div className="flex flex-wrap items-center gap-3 mb-2 text-xs no-print">
                {c.email && <a href={`mailto:${c.email}`} className="font-semibold text-[var(--leon-brown)] hover:underline">✉ {c.email}</a>}
                {c.phone && <a href={`tel:${String(c.phone).replace(/[^0-9+]/g, '')}`} className="font-semibold text-[var(--leon-brown)] hover:underline">✆ {c.phone}</a>}
                {c.mobile && c.mobile !== c.phone && <a href={`tel:${String(c.mobile).replace(/[^0-9+]/g, '')}`} className="font-semibold text-[var(--leon-brown)] hover:underline">✆ {c.mobile} (mobile)</a>}
                {account && (
                  <button onClick={() => ctx.goAccountDetail(account.id)}
                    className="font-semibold text-[var(--leon-brown)] hover:underline ml-auto">
                    Open {account.name} →
                  </button>
                )}
              </div>
            )}
            {/* A contact is a RECORD ON THE ACCOUNT, not text typed onto a job.
                Typing it here meant the same person could exist five different
                ways across five jobs and never join the account's list at all.
                So the details are shown, not typed: pick someone from the list
                above, or add a new one — which files them on the account first
                and then assigns them here. */}
            {c.accountContactId ? (
              <div className="grid sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
                {[
                  ['Company', c.company], ['Contact', c.person], ['Title', c.title],
                  ['Preferred contact', c.preferredContactMethod],
                  ['Phone', c.phone], ['Mobile', c.mobile], ['Email', c.email], ['Notes', c.notes],
                ].map(([label, val]) => (
                  <div key={label}>
                    <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{label}</div>
                    <div className={val ? '' : 'text-[var(--leon-line)]'}>{val || '—'}</div>
                  </div>
                ))}
                {editable && (c.contactAccountId || account) && (
                  <p className="sm:col-span-2 text-[11px] text-[var(--leon-black)]/50 pt-1">
                    These details live on <button className="font-semibold text-[var(--leon-brown)] hover:underline"
                      onClick={() => ctx.goAccountDetail(c.contactAccountId || account.id)}>
                      {c.company || (account && account.name)}
                    </button> &mdash; change them there and every job using this person follows.
                  </p>
                )}
              </div>
            ) : (
              <p className="text-sm text-[var(--leon-black)]/50">
                {c.person
                  ? <>Filled in by hand before contacts were shared: <strong>{c.person}</strong>
                      {c.company ? ` (${c.company})` : ''}. Pick the matching person above, or add them, to
                      link this role to the account&rsquo;s own record.</>
                  : <>No one assigned. Pick someone above, or add a new contact &mdash; they are filed on the
                      account first, so the next job can use them too.</>}
              </p>
            )}
          </EditLock>
          </Collapsible>
        );
      })}

      {project.additionalContacts.map(c => (
        <Collapsible key={c.id} title={c.label} right={editable && <IconBtn title="Remove contact" onClick={() => ctx.removeAdditionalContact(project.id, c.id)}>✕</IconBtn>}>
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Label"><TextInput disabled={!editable} value={c.label} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'label', e.target.value)} /></Field>
            <Field label="Company"><TextInput disabled={!editable} value={c.company} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'company', e.target.value)} /></Field>
            <Field label="Contact Person"><TextInput disabled={!editable} value={c.person} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'person', e.target.value)} /></Field>
            <Field label="Title"><TitleSelect disabled={!editable} value={c.title || ''} onChange={v => ctx.updateAdditionalContact(project.id, c.id, 'title', v)} /></Field>
            <Field label="Preferred Contact Method">
              <Select disabled={!editable} value={c.preferredContactMethod || ''} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'preferredContactMethod', e.target.value)}>
                <option value="">— none set —</option>
                {PREFERRED_CONTACT_METHODS.map(m => <option key={m}>{m}</option>)}
              </Select>
            </Field>
            <Field label="Phone"><TextInput disabled={!editable} value={c.phone} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'phone', e.target.value)} /></Field>
            <Field label="Mobile"><TextInput disabled={!editable} value={c.mobile || ''} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'mobile', e.target.value)} /></Field>
            <Field label="Email"><TextInput disabled={!editable} value={c.email} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'email', e.target.value)} /></Field>
            <Field label="Notes"><TextInput disabled={!editable} value={c.notes || ''} onChange={e => ctx.updateAdditionalContact(project.id, c.id, 'notes', e.target.value)} /></Field>
          </div>
        </Collapsible>
      ))}

      {editable && <div className="flex justify-end"><Button size="sm" onClick={() => setShowAdd(true)}>+ Add Contact</Button></div>}
      <AddProjectContactModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
    </div>
  );
}
// Bring in an existing contact from this project's Account (its Contact
// Log), or type a new one — a newly-typed contact is also logged to that
// Account by default, so it's there to pick from on the account's other
// projects too, not just this one.
function AddProjectContactModal({ open, onClose, ctx, project }) {
  const account = ctx.accounts.find(a => a.id === project.accountId);
  const [mode, setMode] = useState('existing');
  const [existingId, setExistingId] = useState('');
  const blank = { label: '', company: account ? account.name : '', person: '', title: '', phone: '', mobile: '', email: '', preferredContactMethod: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [saveToAccount, setSaveToAccount] = useState(true);
  useEffect(() => {
    if (open) {
      const hasContacts = account && account.contacts.length > 0;
      setMode(hasContacts ? 'existing' : 'new');
      setExistingId(hasContacts ? account.contacts[0].id : '');
      setForm({ ...blank, company: account ? account.name : '' });
      setSaveToAccount(true);
    }
  }, [open, project.accountId]);
  if (!(ctx.accounts || []).length) return null;
  function submit() {
    if (mode === 'existing') {
      const c = account.contacts.find(x => x.id === existingId);
      if (!c) return;
      ctx.addAdditionalContact(project.id, { label: c.role, company: account.name, person: c.name, title: c.title, phone: c.phone, mobile: c.mobile, email: c.email, preferredContactMethod: c.preferredContactMethod, notes: c.notes });
    } else {
      if (!form.person.trim()) return;
      ctx.addAdditionalContact(project.id, form);
      if (saveToAccount) ctx.addAccountContact(account.id, { role: form.label || 'Other', name: form.person, title: form.title, phone: form.phone, mobile: form.mobile, email: form.email, preferredContactMethod: form.preferredContactMethod, notes: form.notes });
    }
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Add Contact" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Contact</Button></>}>
      <div className="space-y-3">
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
          <button onClick={() => setMode('existing')} className={`px-3 py-1.5 rounded-md text-xs font-semibold ${mode === 'existing' ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>Existing Contact</button>
          <button onClick={() => setMode('new')} className={`px-3 py-1.5 rounded-md text-xs font-semibold ${mode === 'new' ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>New Contact</button>
        </div>
        {mode === 'existing' ? (
          account.contacts.length === 0 ? (
            <EmptyState text={`No contacts logged yet for ${account.name} — switch to "New Contact".`} />
          ) : (
            <Field label={`From ${account.name}'s Contact Log`}>
              <Select value={existingId} onChange={e => setExistingId(e.target.value)}>
                {account.contacts.map(c => <option key={c.id} value={c.id}>{c.name} — {c.role}</option>)}
              </Select>
            </Field>
          )
        ) : (
          <>
            <Field label="Label" hint="e.g. Property Manager, Interior Designer…"><TextInput value={form.label} onChange={e => setForm({ ...form, label: e.target.value })} /></Field>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Company"><TextInput value={form.company} onChange={e => setForm({ ...form, company: e.target.value })} /></Field>
              <Field label="Contact Person"><TextInput value={form.person} onChange={e => setForm({ ...form, person: e.target.value })} /></Field>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Title"><TitleSelect value={form.title} onChange={v => setForm({ ...form, title: v })} /></Field>
              <Field label="Preferred Contact Method">
                <Select value={form.preferredContactMethod} onChange={e => setForm({ ...form, preferredContactMethod: e.target.value })}>
                  <option value="">— none set —</option>
                  {PREFERRED_CONTACT_METHODS.map(m => <option key={m}>{m}</option>)}
                </Select>
              </Field>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Phone"><TextInput value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} /></Field>
              <Field label="Mobile"><TextInput value={form.mobile} onChange={e => setForm({ ...form, mobile: e.target.value })} /></Field>
            </div>
            <Field label="Email"><TextInput value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /></Field>
            <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
            <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={saveToAccount} onChange={e => setSaveToAccount(e.target.checked)} /> Also log to {account.name}'s Contact Log</label>
          </>
        )}
      </div>
    </Modal>
  );
}

// ---- Meetings ---------------------------------------------------------------
// Open to any logged-in user (like the Document Library) — not gated by the
// per-module edit-rights matrix, since a meeting log is a shared record
// anyone on the project might need to add to.
function MeetingsTab({ ctx, project }) {
  const [showAdd, setShowAdd] = useState(false);
  const [editingMeeting, setEditingMeeting] = useState(null);
  const [detailForId, setDetailForId] = useState(null);

  // A Meeting logged directly in someone's My To-Do and tied to this
  // project shows up here too, read-only (edited from My To-Do, not this
  // tab). One auto-created FROM a project meeting (sourceMeetingId set) is
  // excluded — the project meeting record itself already represents it, so
  // it isn't shown twice.
  const linkedPersonal = ctx.personalItems.filter(i => i.type === 'Meeting' && i.projectId === project.id && !i.sourceMeetingId);
  const merged = [
    ...project.meetings.map(m => ({ ...m, _source: 'project', _ownerId: null })),
    ...linkedPersonal.map(i => ({
      id: i.id, _source: 'personal', _ownerId: i.userId,
      title: i.title, date: i.date, time: i.time, durationMinutes: i.durationMinutes,
      attendeeIds: [i.userId, ...i.attendeeIds], externalAttendees: '', notes: i.notes,
      loggedBy: i.createdBy, loggedDate: i.createdDate, attachments: i.attachments,
    })),
  ];
  const sorted = [...merged].sort((a, b) => (a.date < b.date ? 1 : -1));
  // Re-derived from `merged` (itself freshly built every render) rather than
  // stored as a snapshot, so editing a project meeting reflects immediately.
  const detailFor = detailForId ? merged.find(m => m.id === detailForId) : null;

  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowAdd(true)}>+ Log Meeting</Button></div>
      <Collapsible title="Meetings" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No meetings logged yet." /> : (
          <div className="space-y-2">
            {sorted.map(m => (
              <div key={m.id} className="border border-[var(--leon-line)] rounded-lg p-3 cursor-pointer" onClick={() => setDetailForId(m.id)}>
                <div className="flex items-center gap-2 flex-wrap">
                  <p className="text-sm font-semibold hover:underline">{m.title || 'Untitled Meeting'}</p>
                  <Badge tone="neutral">{fmtDate(m.date)}{m.time ? ` · ${fmtTimeRange(m.time, m.durationMinutes)}` : ''}</Badge>
                  {m._source === 'personal' && <Badge tone="yellow">From {personName(ctx.teamDirectory, m._ownerId)}'s My To-Do</Badge>}
                </div>
                <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">
                  {m.attendeeIds.map(id => personName(ctx.teamDirectory, id)).join(', ')}{m.externalAttendees ? `${m.attendeeIds.length ? ', ' : ''}${m.externalAttendees}` : ''}
                </p>
                {m.notes && <p className="text-xs text-[var(--leon-black)]/60 mt-1 line-clamp-2">{m.notes}</p>}
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <AddMeetingModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <AddMeetingModal open={!!editingMeeting} meeting={editingMeeting} onClose={() => setEditingMeeting(null)} ctx={ctx} project={project} />
      <MeetingDetailModal open={!!detailFor} meeting={detailFor} onClose={() => setDetailForId(null)}
        onEdit={detailFor && detailFor._source === 'project' ? () => { setEditingMeeting(detailFor); setDetailForId(null); } : null}
        ctx={ctx} project={project} />
    </div>
  );
}
// Handles both Add (meeting=null) and Edit (meeting set) — same fields
// either way, mirroring the My To-Do add/edit form pattern.
function AddMeetingModal({ open, meeting, onClose, ctx, project }) {
  const isEdit = !!meeting;
  const blank = { title: '', date: todayISO(), time: '', durationMinutes: null, attendeeIds: [], externalAttendees: '', notes: '', attachments: [] };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(meeting ? { ...blank, ...meeting } : blank); }, [open, meeting]);
  function submit() {
    if (isEdit) ctx.updateMeeting(project.id, meeting.id, form);
    else ctx.addMeeting(project.id, form);
    onClose();
  }
  function toggleAttendee(id) { setForm(f => ({ ...f, attendeeIds: f.attendeeIds.includes(id) ? f.attendeeIds.filter(x => x !== id) : [...f.attendeeIds, id] })); }
  function addAttachment(fname, url) { setForm(f => ({ ...f, attachments: [...f.attachments, { id: uid('att'), name: fname, url, uploadedBy: ctx.currentUserName, uploadedDate: todayISO() }] })); }
  function removeAttachment(id) { setForm(f => ({ ...f, attachments: f.attachments.filter(a => a.id !== id) })); }
  return (
    <Modal open={open} onClose={onClose} wide title={isEdit ? 'Edit Meeting' : 'Log Meeting'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Log Meeting'}</Button></>}>
      <div className="space-y-3">
        <Field label="Title"><TextInput value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} placeholder="e.g. Weekly Site Coordination" /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.time} onChange={e => setForm({ ...form, time: e.target.value, durationMinutes: e.target.value ? form.durationMinutes : null })} /></Field>
          <Field label="Duration">
            <Select value={form.durationMinutes || ''} onChange={e => setForm({ ...form, durationMinutes: e.target.value ? Number(e.target.value) : null })} disabled={!form.time}>
              <option value="">—</option>
              {PERSONAL_ITEM_DURATIONS.map(m => <option key={m} value={m}>{durationLabel(m)}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Attendees (Team)">
          <div className="flex flex-wrap gap-1.5 border border-[var(--leon-line)] rounded-lg p-2 max-h-28 overflow-y-auto">
            {ctx.teamDirectory.filter(p => p.active).map(p => (
              <label key={p.id} className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border cursor-pointer ${form.attendeeIds.includes(p.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
                <input type="checkbox" className="hidden" checked={form.attendeeIds.includes(p.id)} onChange={() => toggleAttendee(p.id)} /> {p.name}
              </label>
            ))}
          </div>
        </Field>
        <Field label="Other Attendees" hint="Client, subcontractor, or anyone outside the team directory"><TextInput value={form.externalAttendees} onChange={e => setForm({ ...form, externalAttendees: e.target.value })} /></Field>
        <Field label="Meeting Notes"><TextArea rows={5} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <Field label="Attachments">
          <div className="space-y-1.5">
            {form.attachments.map(a => (
              <div key={a.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded-md px-2 py-1">
                <span>{a.name}</span>
                <IconBtn title="Remove" onClick={() => removeAttachment(a.id)}>✕</IconBtn>
              </div>
            ))}
            <FileField name="" url={null} onChange={addAttachment} editable placeholder="+ Add attachment" />
          </div>
        </Field>
      </div>
    </Modal>
  );
}
function MeetingDetailModal({ open, meeting, onClose, onEdit, ctx, project }) {
  if (!meeting) return null;
  const isPersonal = meeting._source === 'personal';
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Meeting — ${meeting.title || 'Untitled'}`} printable
      fields={[
        { label: 'Date', value: fmtDate(meeting.date) }, { label: 'Time', value: fmtTimeRange(meeting.time, meeting.durationMinutes) || '—' },
        { label: 'Attendees (Team)', value: meeting.attendeeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') || '—' },
        { label: 'Other Attendees', value: meeting.externalAttendees || '—' },
        { label: 'Logged By', value: `${meeting.loggedBy} — ${fmtDate(meeting.loggedDate)}` },
        ...(isPersonal ? [{ label: 'Source', value: "Logged in My To-Do, linked to this project" }] : []),
        { label: 'Notes', value: meeting.notes },
      ]}
      attachments={meeting.attachments}
    >
      {onEdit && <div className="no-print"><Button size="sm" variant="outline" onClick={onEdit}>Edit</Button></div>}
      {!isPersonal && (
        <div className="no-print">
          <p className="text-xs font-semibold mb-1">Add Attachment</p>
          <div className="space-y-1.5">
            {meeting.attachments.map(a => (
              <div key={a.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded-md px-2 py-1">
                <span>{a.name}</span>
                <IconBtn title="Remove" onClick={() => ctx.removeMeetingAttachment(project.id, meeting.id, a.id)}>✕</IconBtn>
              </div>
            ))}
            <FileField name="" url={null} onChange={(fname, url) => ctx.addMeetingAttachment(project.id, meeting.id, fname, url)} editable placeholder="+ Add attachment" />
          </div>
        </div>
      )}
    </RecordDetailModal>
  );
}

// ---- Jobsite Visits ---------------------------------------------------------
function JobsiteVisitsTab({ ctx, project }) {
  const [showAdd, setShowAdd] = useState(false);
  const [editingVisit, setEditingVisit] = useState(null);
  const sorted = [...project.jobsiteVisits].sort((a, b) => (a.date < b.date ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowAdd(true)}>+ Log Visit</Button></div>
      <Collapsible title="Jobsite Visits" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No jobsite visits logged yet." /> : (
          <div className="space-y-2">
            {sorted.map(v => <JobsiteVisitCard key={v.id} ctx={ctx} project={project} visit={v} allVisits={project.jobsiteVisits} onEdit={() => setEditingVisit(v)} />)}
          </div>
        )}
      </Collapsible>
      <AddJobsiteVisitModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <AddJobsiteVisitModal open={!!editingVisit} visit={editingVisit} onClose={() => setEditingVisit(null)} ctx={ctx} project={project} />
    </div>
  );
}
function JobsiteVisitCard({ ctx, project, visit, allVisits, onEdit }) {
  const isFollowUp = !!visit.followUpFromVisitId;
  const scheduledFollowUp = allVisits.find(v => v.followUpFromVisitId === visit.id);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-start justify-between gap-2 flex-wrap">
        <div>
          <div className="flex items-center gap-2 flex-wrap">
            <p className="text-sm font-semibold">{visit.purpose || 'Jobsite Visit'}</p>
            <Badge tone="neutral">{fmtDate(visit.date)}{visit.time ? ` · ${fmtTimeRange(visit.time, visit.durationMinutes)}` : ''}</Badge>
            {isFollowUp && <Badge tone="blue">Follow-up</Badge>}
          </div>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Visited by: {personName(ctx.teamDirectory, visit.assigneeId)}</p>
          {visit.notes && <p className="text-xs text-[var(--leon-black)]/60 mt-1">{visit.notes}</p>}
          {scheduledFollowUp && <p className="text-xs text-[var(--leon-brown)] font-semibold mt-1">🔁 Follow-up scheduled — {fmtDate(scheduledFollowUp.date)}, {personName(ctx.teamDirectory, scheduledFollowUp.assigneeId)}</p>}
        </div>
        <div className="flex items-center gap-2 shrink-0">
          <button onClick={onEdit} className="text-xs text-[var(--leon-brown)] font-semibold">✎ Edit</button>
          <IconBtn title="Remove" onClick={() => ctx.removeJobsiteVisit(project.id, visit.id)}>✕</IconBtn>
        </div>
      </div>
      <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Photos</p>
        <div className="flex items-center gap-2 flex-wrap">
          {visit.pictures.map((url, i) => <ClickableImage key={i} src={url} name={`Photo ${i + 1}`} className="w-14 h-14 object-cover rounded-md" />)}
          <ImagePicker url={null} onChange={url => ctx.addJobsiteVisitPicture(project.id, visit.id, url)} size={56} />
        </div>
      </div>
    </div>
  );
}
// Handles both Add (visit=null) and Edit (visit set), same pattern as
// AddMeetingModal — plus photos captured right in the same form (not only
// after the fact from the card), and an optional "needs a follow-up"
// section that, on save, automatically logs a second visit in the future
// assigned to whoever should do it, linked back via followUpFromVisitId.
function AddJobsiteVisitModal({ open, visit, onClose, ctx, project }) {
  const isEdit = !!visit;
  const blank = { date: todayISO(), time: '', durationMinutes: '', assigneeId: '', purpose: '', notes: '', pictures: [] };
  const [form, setForm] = useState(blank);
  const [needsFollowUp, setNeedsFollowUp] = useState(false);
  const [followUpDate, setFollowUpDate] = useState('');
  const [followUpAssigneeId, setFollowUpAssigneeId] = useState('');
  useEffect(() => {
    if (open) {
      setForm(visit ? { date: visit.date, time: visit.time, durationMinutes: visit.durationMinutes || '', assigneeId: visit.assigneeId || '', purpose: visit.purpose, notes: visit.notes, pictures: visit.pictures || [] } : blank);
      setNeedsFollowUp(false);
      setFollowUpDate('');
      setFollowUpAssigneeId('');
    }
  }, [open, visit]);
  function addPic(url) { setForm({ ...form, pictures: [...form.pictures, url] }); }
  function removePic(i) { setForm({ ...form, pictures: form.pictures.filter((_, idx) => idx !== i) }); }
  function submit() {
    const data = { ...form, durationMinutes: form.durationMinutes ? Number(form.durationMinutes) : null, assigneeId: form.assigneeId || null };
    let visitId = visit ? visit.id : null;
    if (isEdit) ctx.updateJobsiteVisit(project.id, visit.id, data);
    else visitId = ctx.addJobsiteVisit(project.id, data);
    if (needsFollowUp && followUpDate) {
      ctx.addJobsiteVisit(project.id, {
        date: followUpDate, assigneeId: followUpAssigneeId || form.assigneeId || null,
        purpose: form.purpose ? `Follow-up: ${form.purpose}` : 'Follow-up Visit',
        notes: 'Automatically scheduled as a follow-up.',
        followUpFromVisitId: visitId,
      });
    }
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={isEdit ? 'Edit Jobsite Visit' : 'Log Jobsite Visit'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.time} onChange={e => setForm({ ...form, time: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Duration (minutes)"><TextInput type="number" value={form.durationMinutes} onChange={e => setForm({ ...form, durationMinutes: e.target.value })} /></Field>
          <Field label="Assignee"><Select value={form.assigneeId} onChange={e => setForm({ ...form, assigneeId: e.target.value })}><option value="">— select —</option>{ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
        </div>
        <Field label="Purpose"><TextInput value={form.purpose} onChange={e => setForm({ ...form, purpose: e.target.value })} placeholder="e.g. Quality walk-through" /></Field>
        <Field label="Notes"><TextArea rows={3} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <div>
          <p className="text-xs font-semibold mb-1">Photos</p>
          <div className="flex items-center gap-2 flex-wrap">
            {form.pictures.map((p, i) => (
              <span key={i} className="relative">
                <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                <button type="button" onClick={() => removePic(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
              </span>
            ))}
            <ImagePicker url={null} onChange={addPic} size={56} />
          </div>
        </div>
        <div className="border-t border-[var(--leon-line)] pt-3">
          <label className="flex items-center gap-2 text-sm font-semibold cursor-pointer">
            <input type="checkbox" checked={needsFollowUp} onChange={e => setNeedsFollowUp(e.target.checked)} />
            This visit needs a follow-up
          </label>
          {needsFollowUp && (
            <div className="grid grid-cols-2 gap-3 mt-2">
              <Field label="Follow-up Date"><TextInput type="date" value={followUpDate} onChange={e => setFollowUpDate(e.target.value)} /></Field>
              <Field label="Assign To" hint="Defaults to this visit's assignee"><Select value={followUpAssigneeId || form.assigneeId} onChange={e => setFollowUpAssigneeId(e.target.value)}><option value="">— select —</option>{ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
}

// ---- Scopes & Schedule -----------------------------------------------------
const SCOPES_VIEW_MODES = ['List', 'Timeline'];
// Schedule by Stages — Overview (every stage: project-level Lead/Take-Off/
// Quotation chronology plus every scope's own stages) alongside dedicated
// Take-Off and Quotation Stage subtabs, then one subtab per scope for its
// contract-onward stages. Mirrors the Phase 4 Hub scope-subtab pattern.
const SCHEDULE_BY_STAGES_FIXED_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'takeOff', label: 'Take-Off', icon: '📏' },
  { key: 'quotation', label: 'Quotation Stage', icon: '💬' },
];
function ScopesTab({ ctx, project }) {
  const [sub, setSub] = useState('overview');
  // Only the active department's scopes are listed — a mixed project stays
  // visible to both departments, but each sees only its own work.
  const visibleScopes = ctx.deptScopes(project);
  const tabs = [...SCHEDULE_BY_STAGES_FIXED_SUBTABS, ...visibleScopes.map(s => ({ key: s.id, label: s.name }))];
  const scope = visibleScopes.find(s => s.id === sub);
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />
      {sub === 'overview' && <ScopesOverviewSubTab ctx={ctx} project={project} />}
      {sub === 'takeOff' && <TakeOffStageSubTab ctx={ctx} project={project} />}
      {sub === 'quotation' && <QuotationStageSubTab ctx={ctx} project={project} />}
      {scope && <ScopeBlock ctx={ctx} project={project} scope={scope} editable={ctx.canEdit('scopes')} />}
    </div>
  );
}
// Exactly the tab's old (pre-restructure) content, unchanged — List/Timeline
// toggle, Add Scope — with the project's own Chronology table now prepended
// to List view so "all the stages" really does mean all of them, not just
// each scope's contract-onward ones.
function ScopesOverviewSubTab({ ctx, project }) {
  const [showAdd, setShowAdd] = useState(false);
  const [view, setView] = useState('List');
  const editable = ctx.canEdit('scopes');
  // Department-scoped, same as ScopesTab above.
  const visibleScopes = ctx.deptScopes(project);
  return (
    <div>
      <div className="flex items-center justify-between mb-2 gap-2 flex-wrap">
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {SCOPES_VIEW_MODES.map(v => (
            <button key={v} onClick={() => setView(v)} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors ${view === v ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>{v}</button>
          ))}
        </div>
        {editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Scope</Button>}
      </div>
      {view === 'List' ? (
        <>
          <ChronologySubTab ctx={ctx} project={project} />
          {visibleScopes.length === 0 ? <EmptyState text={ctx.activeDepartment === ALL_DEPARTMENTS ? 'No scopes yet.' : `No ${ctx.activeDepartment} scopes on this project.`} /> : visibleScopes.map(scope => (
            <ScopeBlock key={scope.id} ctx={ctx} project={project} scope={scope} editable={editable} />
          ))}
        </>
      ) : <ScopesTimelineView project={project} />}
      <AddScopeModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
    </div>
  );
}
// A chronology stage's own key for its base entry, or `<key>_revision_N`
// for an inserted revision round (see insertRevisionStage, lib.jsx) — this
// is how the Take-Off/Quotation subtabs pick out just their own family's
// rows from the single shared project.chronology array.
function chronologyFamilyStages(project, keys) {
  return project.chronology.filter(st => keys.some(k => st.key === k || st.key.startsWith(`${k}_revision_`)));
}
function TakeOffStageSubTab({ ctx, project }) {
  const stages = chronologyFamilyStages(project, ['take_off']);
  const done = stages.filter(s => s.status === 'Completed').length;
  return (
    <Collapsible title="Take-Off Stage" count={stages.length} right={stages.length ? <span className="text-xs text-[var(--leon-black)]/40">{done}/{stages.length} complete</span> : null}>
      {stages.length === 0 ? <EmptyState text="No Take-Off stage yet." /> : <ChronologyStagesTable ctx={ctx} project={project} stages={stages} />}
    </Collapsible>
  );
}
function QuotationStageSubTab({ ctx, project }) {
  const stages = chronologyFamilyStages(project, ['quote_prep', 'quote_revision']);
  const done = stages.filter(s => s.status === 'Completed').length;
  return (
    <Collapsible title="Quotation Stage" count={stages.length} right={stages.length ? <span className="text-xs text-[var(--leon-black)]/40">{done}/{stages.length} complete</span> : null}>
      {stages.length === 0 ? <EmptyState text="No Quotation stage yet." /> : <ChronologyStagesTable ctx={ctx} project={project} stages={stages} />}
    </Collapsible>
  );
}
// Horizontal Gantt-style read of the same stage dates ScopeBlock's table
// shows — one row per stage, positioned/sized against the project's overall
// date range, colored by status. Bars use plannedStart/plannedDue (not
// actuals) since a stage without actuals yet still needs to show on the
// timeline as scheduled.
// Stage name lives OUTSIDE the bar in a fixed label column — text crammed
// inside a short bar was unreadable for any stage under a few weeks long,
// which is most of them. Color only needs to carry status now; full-height
// month gridlines make it possible to actually read a date off the chart
// instead of only the header row.
function ScopesTimelineView({ project }) {
  const allStages = [...project.chronology, ...project.scopes.flatMap(s => s.stages)].filter(st => st.plannedStart && st.plannedDue);
  if (allStages.length === 0) return <EmptyState text="No scheduled stages yet." />;
  const starts = allStages.map(st => st.plannedStart);
  const ends = allStages.map(st => st.plannedDue);
  const minDate = addDays(starts.reduce((a, b) => (a < b ? a : b)), -3);
  const maxDate = addDays(ends.reduce((a, b) => (a > b ? a : b)), 3);
  const totalDays = Math.max(1, daysBetween(minDate, maxDate));
  const today = todayISO();
  const pctFor = dateStr => Math.max(0, Math.min(100, (daysBetween(minDate, dateStr) / totalDays) * 100));
  const LABEL_W = 220;
  // Distinct hues per status rather than shades of the app's own brown
  // accent, which read as "just UI chrome" rather than a status signal.
  // Row status -> shared schedule palette key.
  const BAR_STATUS = { Completed: 'Complete', Overdue: 'Delayed', 'In Progress': 'In Progress', 'Not Started': 'Not Started' };
  function statusFor(st) {
    if (st.status === 'Completed') return 'Completed';
    if (st.plannedDue < today) return 'Overdue';
    if (st.status === 'In Progress') return 'In Progress';
    return 'Not Started';
  }
  const months = [];
  let cursor = fromISO(minDate);
  cursor.setDate(1);
  const end = fromISO(maxDate);
  while (cursor <= end) {
    months.push(toISO(cursor));
    cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1);
  }
  const todayInRange = today >= minDate && today <= maxDate;
  const chronologyGroup = { scope: { id: 'chronology', name: 'Lead, Take-Off & Quotes' }, stages: project.chronology.filter(st => st.plannedStart && st.plannedDue) };
  const scopesWithStages = [chronologyGroup, ...project.scopes.map(scope => ({ scope, stages: scope.stages.filter(st => st.plannedStart && st.plannedDue) }))].filter(g => g.stages.length > 0);
  // Grouped-by-scope, collapsed-by-default: each scope gets its own
  // Collapsible with its own month header + gridlines drawn to the SAME
  // minDate/maxDate/pctFor scale computed above, so bars stay comparable
  // across scopes even though each group renders independently.
  return (
    <div>
      {scopesWithStages.map(({ scope, stages }) => {
        const overdueCount = stages.filter(st => statusFor(st) === 'Overdue').length;
        return (
          <Collapsible
            key={scope.id}
            title={scope.name}
            count={stages.length}
            right={overdueCount > 0 && <Badge tone="red">{overdueCount} overdue</Badge>}
          >
      <div className="overflow-x-auto border border-[var(--leon-line)] rounded-xl bg-white">
              <div style={{ minWidth: '900px' }}>
                <div className="flex border-b border-[var(--leon-line)]">
                  <div className="shrink-0 border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} />
                  <div className="relative flex-1 h-8">
                    {months.map(m => (
                      <div key={m} className="absolute top-0 h-full flex items-center text-[11px] font-bold text-[var(--leon-black)]/60 uppercase border-l border-[var(--leon-line)] pl-1.5" style={{ left: `${pctFor(m)}%` }}>
                        {fromISO(m).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })}
                      </div>
                    ))}
                  </div>
                </div>
                <div className="relative">
                  <div className="absolute inset-0 pointer-events-none" style={{ left: LABEL_W }}>
                    {months.map(m => <div key={m} className="absolute top-0 bottom-0 border-l border-[var(--leon-line)]" style={{ left: `${pctFor(m)}%` }} />)}
                    {todayInRange && <div className="absolute top-0 bottom-0 w-0.5 bg-[var(--leon-red)]" style={{ left: `${pctFor(today)}%` }} />}
                  </div>
                  {stages.map((st, si) => {
                    const status = statusFor(st);
                    const color = scheduleGradient(BAR_STATUS[status]);
                    const leftPct = pctFor(st.plannedStart);
                    const widthPct = Math.max(0.6, pctFor(st.plannedDue) - leftPct);
                    return (
                      <div key={st.id} className="flex items-center hover:bg-[var(--leon-cream)]/70" style={{ background: si % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                        <div className="shrink-0 pl-3 pr-3 py-1 text-xs text-[var(--leon-black)]/80 truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={st.name}>{st.name}</div>
                        <div className="relative flex-1" style={{ height: 30 }}>
                          <div
                            className="absolute rounded-md shadow-sm"
                            style={{ left: `${leftPct}%`, width: `${widthPct}%`, top: 6, height: 18, minWidth: 8, background: color, boxShadow: '0 1px 3px rgba(22,19,17,.18)', borderRadius: 999 }}
                            title={`${st.name}: ${fmtDate(st.plannedStart)} – ${fmtDate(st.plannedDue)} (${status})`}
                          />
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            </div>
          </Collapsible>
        );
      })}
      <div className="flex items-center gap-4 px-1 py-2 text-[11px] text-[var(--leon-black)]/60 flex-wrap">
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(BAR_STATUS['Not Started']) }} /> Not Started</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(BAR_STATUS['In Progress']) }} /> In Progress</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(BAR_STATUS['Completed']) }} /> Completed</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(BAR_STATUS['Overdue']) }} /> Overdue</span>
        <span className="flex items-center gap-1.5"><span className="w-0.5 h-3 bg-[var(--leon-red)] inline-block" /> Today</span>
      </div>
    </div>
  );
}

// Project-level counterpart to ScopeBlock's stage table (below), for
// Lead Review/Take-Off/Quote Prep/Quote Revision — tracked once per
// project instead of duplicated per scope. Same Start/Complete/Report
// Delay/Undo/Add Revision actions, reusing ReportDelayModal as-is (it's
// already generic over any {stage} object). Followed by the existing
// per-scope rollup so the "overview of the chronology" shows both levels
// together in one place.
// Shared table body for any subset of project.chronology — the full array
// (ChronologySubTab, Schedule by Stages' Overview) or just one family's rows
// (TakeOffStageSubTab/QuotationStageSubTab). Owns its own Report Delay modal
// so every caller gets Start/Complete/Report Delay/Undo/+Add Revision as-is.
function ChronologyStagesTable({ ctx, project, stages }) {
  const [delayFor, setDelayFor] = useState(null);
  const editable = ctx.canEdit('scopes');
  const lastChange = project.chronologyHistory && project.chronologyHistory.length ? project.chronologyHistory[project.chronologyHistory.length - 1] : null;
  return (
    <>
      <div className="overflow-x-auto">
        <table className="w-full text-xs">
          <thead>
            <tr className="text-left text-[var(--leon-black)]/40 uppercase tracking-wide">
              <th className="py-1.5 pr-2">Stage</th>
              <th className="py-1.5 pr-2">Assigned To</th>
              <th className="py-1.5 pr-2">Planned Start</th>
              <th className="py-1.5 pr-2">Planned Due</th>
              <th className="py-1.5 pr-2">Actual Start</th>
              <th className="py-1.5 pr-2">Actual Complete</th>
              <th className="py-1.5 pr-2">Status</th>
              <th className="py-1.5 pr-2">Delay</th>
              {editable && <th className="py-1.5 pr-2">Undo</th>}
              {editable && <th className="py-1.5 pr-2"></th>}
            </tr>
          </thead>
          <tbody>
            {stages.map(st => (
              <tr key={st.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{st.name}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">
                  {editable && ctx.canAssignStage ? (
                    <Select value={st.assignedUserId || ''} onChange={e => ctx.assignChronologyUser(project.id, st.id, e.target.value || null)} className="!py-0.5 !text-xs !w-36">
                      <option value="">{st.responsibleRole ? `Unassigned (${st.responsibleRole})` : 'Unassigned'}</option>
                      {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                  ) : (
                    st.assignedUserId ? personName(ctx.teamDirectory, st.assignedUserId) : <span className="text-[var(--leon-black)]/40">{st.responsibleRole ? `Unassigned (${st.responsibleRole})` : 'Unassigned'}</span>
                  )}
                </td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.plannedStart)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.plannedDue)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.actualStart)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.actualCompletion)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap"><StatusBadge status={st.status} /></td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{st.delayDays > 0 ? <span className="text-[var(--leon-red)] font-semibold">+{st.delayDays}d</span> : '—'}{st.delayReason && <div className="text-[10px] text-[var(--leon-black)]/40">{st.delayReason}</div>}</td>
                {editable && (
                  <td className="py-1.5 pr-2 whitespace-nowrap">
                    {lastChange && lastChange.stageId === st.id && (
                      <button onClick={() => ctx.undoChronologyChange(project.id)} title="Undoes the most recent Start / Complete / Report Delay on this stage." className="no-print text-xs text-[var(--leon-brown)] font-semibold">↩ Undo</button>
                    )}
                  </td>
                )}
                {editable && (
                  <td className="py-1.5 pr-2 whitespace-nowrap">
                    <div className="flex gap-1">
                      {st.status === 'Not Started' && <Button size="sm" variant="ghost" onClick={() => ctx.startChronologyStage(project.id, st.id)}>Start</Button>}
                      {st.status !== 'Completed' && <Button size="sm" variant="ghost" onClick={() => ctx.completeChronologyStage(project.id, st.id)}>Complete</Button>}
                      {st.status !== 'Completed' && <Button size="sm" variant="ghost" onClick={() => setDelayFor(st)}>Report Delay</Button>}
                      {REVISION_STAGE_FAMILIES[st.key] && (
                        <Button size="sm" variant="ghost" title={`Adds another round of ${REVISION_STAGE_FAMILIES[st.key].label} — downstream stages shift to make room.`} onClick={() => ctx.addRevisionStage(project.id, null, st.key)}>+ Add Revision</Button>
                      )}
                    </div>
                  </td>
                )}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <ReportDelayModal open={!!delayFor} stage={delayFor} onClose={() => setDelayFor(null)} onSubmit={(days, reason, note) => { ctx.reportChronologyDelay(project.id, delayFor.id, days, reason, note); setDelayFor(null); }} />
    </>
  );
}
function ChronologySubTab({ ctx, project }) {
  const chronology = project.chronology;
  const done = chronology.filter(s => s.status === 'Completed').length;
  return (
    <div>
      <Collapsible title="Lead, Take-Off & Quotes" count={chronology.length} right={<span className="text-xs text-[var(--leon-black)]/40">{done}/{chronology.length} complete</span>}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">Shared once per project — these stages cover the full project, not any one scope.</p>
        <ChronologyStagesTable ctx={ctx} project={project} stages={chronology} />
      </Collapsible>
    </div>
  );
}
function ScopeBlock({ ctx, project, scope, editable }) {
  const [delayFor, setDelayFor] = useState(null);
  // A revision round is named after the stage it revises, and that stage's
  // name is the template's, not the catalogue's — "Submittal" on a countertop.
  // How this scope is actually sold — a single-package family (countertops)
  // overrides whatever string is stored on the scope. Badges and the
  // printed letterhead read this, never scope.scopeType.
  const soldAs = scopeSoldAs(scope, ctx.scopeLibrary);
  const templateKey = scopeStageTemplateKey(scope.familyName, ctx.scopeLibrary, soldAs);
  const revisionLabel = key => key === 'shop_drawings'
    ? templateStageName(templateKey, 'shop_drawing_revision', REVISION_STAGE_FAMILIES[key].label)
    : REVISION_STAGE_FAMILIES[key].label;
  const delay = scopeTotalDelayDays(scope);
  const done = scope.stages.filter(s => s.status === 'Completed').length;
  // The most recent change (if any) names exactly one stage — Undo is shown
  // only on that stage's own row (§ per-stage undo request), not as one
  // shared control for the whole scope.
  const lastChange = scope.stageHistory && scope.stageHistory.length ? scope.stageHistory[scope.stageHistory.length - 1] : null;

  return (
    <Collapsible
      printRegion
      printable={false}
      title={scope.name}
      count={scope.stages.length}
      right={
        <div className="flex items-center gap-2">
          {delay > 0 && <Badge tone="red">+{delay}d total delay</Badge>}
          <Badge tone="neutral">{scope.familyName}</Badge>
          <span onClick={e => e.stopPropagation()}><DocActions title={`${project.name} — ${scope.name}`}
            heading={scope.name} lines={[project.name, project.projectNumber, scope.familyName, soldAs].filter(Boolean)} /></span>
          {soldAs === 'Supply Only' && <Badge tone="yellow">Supply Only</Badge>}
          {soldAs === 'Labor Only' && <Badge tone="blue">Labor Only</Badge>}
          {soldAs === COMBINED_SCOPE_TYPE && <Badge tone="brown">{scopeTypeLabel(scope.familyName, ctx.scopeLibrary, soldAs)}</Badge>}
          <span className="text-xs text-[var(--leon-black)]/40">{done}/{scope.stages.length} complete</span>
          {/* Everything filed against this scope, grouped by section — tick a
              whole section, or open it and pick individual files. */}
          <span onClick={e => e.stopPropagation()}>
            <ShareButton ctx={ctx} projectId={project.id} subjectKey={`scope:${scope.id}`}
              subject={`${scope.name} — ${project.name}`}
              summary={`${scope.familyName || ''}${scope.quantity ? ` · ${scope.quantity} ${scope.unit || ''}` : ''}`.replace(/^ · /, '')}
              items={scopePackageItems(project, scope)} />
          </span>
        </div>
      }
    >
      <p className="text-xs text-[var(--leon-black)]/50 mb-1">Projected completion: <strong>{fmtDate(scopeProjectedCompletion(scope))}</strong></p>
      <div className="flex items-center gap-2 mb-2 text-xs text-[var(--leon-black)]/50">
        <span>Quantity:</span>
        {editable ? (
          <>
            <TextInput type="number" defaultValue={scope.quantity ?? ''} onBlur={e => ctx.updateScopeQuantity(project.id, scope.id, e.target.value ? Number(e.target.value) : null, scope.unit)} className="!w-20 !py-0.5 !text-xs" />
            <Select value={scope.unit} onChange={e => ctx.updateScopeQuantity(project.id, scope.id, scope.quantity, e.target.value)} className="!w-28 !py-0.5 !text-xs">
              {UNIT_TYPES.map(u => <option key={u}>{u}</option>)}
            </Select>
          </>
        ) : <strong>{scope.quantity ?? '—'} {scope.quantity ? scope.unit : ''}</strong>}
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-xs">
          <thead>
            <tr className="text-left text-[var(--leon-black)]/40 uppercase tracking-wide">
              <th className="py-1.5 pr-2">Stage</th>
              <th className="py-1.5 pr-2">Assigned To</th>
              <th className="py-1.5 pr-2">Planned Start</th>
              <th className="py-1.5 pr-2">Planned Due</th>
              <th className="py-1.5 pr-2">Actual Start</th>
              <th className="py-1.5 pr-2">Actual Complete</th>
              <th className="py-1.5 pr-2">Status</th>
              <th className="py-1.5 pr-2">Delay</th>
              {editable && <th className="py-1.5 pr-2">Undo</th>}
              {editable && <th className="py-1.5 pr-2"></th>}
            </tr>
          </thead>
          <tbody>
            {scope.stages.map(st => (
              <tr key={st.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{st.name}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">
                  {editable && ctx.canAssignStage ? (
                    <Select value={st.assignedUserId || ''} onChange={e => ctx.assignStageUser(project.id, scope.id, st.id, e.target.value || null)} className="!py-0.5 !text-xs !w-36">
                      <option value="">{st.responsibleRole ? `Unassigned (${st.responsibleRole})` : 'Unassigned'}</option>
                      {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                  ) : (
                    st.assignedUserId ? personName(ctx.teamDirectory, st.assignedUserId) : <span className="text-[var(--leon-black)]/40">{st.responsibleRole ? `Unassigned (${st.responsibleRole})` : 'Unassigned'}</span>
                  )}
                </td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.plannedStart)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.plannedDue)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.actualStart)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{fmtDate(st.actualCompletion)}</td>
                <td className="py-1.5 pr-2 whitespace-nowrap"><StatusBadge status={st.status} /></td>
                <td className="py-1.5 pr-2 whitespace-nowrap">{st.delayDays > 0 ? <span className="text-[var(--leon-red)] font-semibold">+{st.delayDays}d</span> : '—'}{st.delayReason && <div className="text-[10px] text-[var(--leon-black)]/40">{st.delayReason}</div>}</td>
                {editable && (
                  <td className="py-1.5 pr-2 whitespace-nowrap">
                    {lastChange && lastChange.stageId === st.id && (
                      <button
                        onClick={() => ctx.undoStageChange(project.id, scope.id)}
                        title="Undoes the most recent Start / Complete / Report Delay on this stage."
                        className="no-print text-xs text-[var(--leon-brown)] font-semibold"
                      >
                        ↩ Undo
                      </button>
                    )}
                  </td>
                )}
                {editable && (
                  <td className="py-1.5 pr-2 whitespace-nowrap">
                    <div className="flex gap-1">
                      {st.status === 'Not Started' && <Button size="sm" variant="ghost" onClick={() => ctx.startStage(project.id, scope.id, st.id)}>Start</Button>}
                      {st.status !== 'Completed' && <Button size="sm" variant="ghost" onClick={() => ctx.completeStage(project.id, scope.id, st.id)}>Complete</Button>}
                      {st.status !== 'Completed' && <Button size="sm" variant="ghost" onClick={() => setDelayFor(st)}>Report Delay</Button>}
                      {REVISION_STAGE_FAMILIES[st.key] && (
                        <Button size="sm" variant="ghost" title={`Adds a round of ${revisionLabel(st.key)} after this stage — downstream stages shift to make room. A revision is raised when it is needed, which is why one is not carried in every schedule from the start.`} onClick={() => ctx.addRevisionStage(project.id, scope.id, st.key, revisionLabel(st.key))}>+ Add Revision</Button>
                      )}
                    </div>
                  </td>
                )}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <ReportDelayModal open={!!delayFor} stage={delayFor} onClose={() => setDelayFor(null)} onSubmit={(days, reason, note) => { ctx.reportDelay(project.id, scope.id, delayFor.id, days, reason, note); setDelayFor(null); }} />
      {scope.windowSchedule && <WindowScheduleSection ctx={ctx} project={project} scope={scope} editable={editable} />}
    </Collapsible>
  );
}

// ---------------------------------------------------------------------------
// Window & Exterior Door System schedule — scope-detail section (§ Window
// Schedule template). Rendered only when scope.windowSchedule exists,
// alongside (not replacing) the stage table above.
// ---------------------------------------------------------------------------
// Gantt-style dashboard for a window scope's dependency graph — same
// hand-rolled percentage-math technique as ScopesTimelineView (no SVG,
// no external library): a shared linear day-scale mapped to a 0-100% axis,
// bars positioned by left%/width%. Adds a baseline "ghost" bar underneath
// each forecast bar and a solid actual-complete marker, plus a critical-
// path border treatment — nothing this app didn't already do elsewhere,
// just applied to a DAG's nodes instead of a linear stage list.
function WindowScheduleGanttView({ ws, criticalPath }) {
  const withDates = ws.nodes.filter(n => n.forecastStart && n.forecastEnd);
  if (withDates.length === 0) return <EmptyState text="Window Schedule not yet computed — pick a System when adding this scope." />;
  const starts = withDates.map(n => n.forecastStart);
  const ends = withDates.map(n => n.forecastEnd);
  const minDate = addDays(starts.reduce((a, b) => (a < b ? a : b)), -3);
  const maxDate = addDays(ends.reduce((a, b) => (a > b ? a : b)), 3);
  const totalDays = Math.max(1, daysBetween(minDate, maxDate));
  const today = todayISO();
  const pctFor = d => Math.max(0, Math.min(100, (daysBetween(minDate, d) / totalDays) * 100));
  const LABEL_W = 220;
  const months = [];
  let cursor = fromISO(minDate);
  cursor.setDate(1);
  const end = fromISO(maxDate);
  while (cursor <= end) { months.push(toISO(cursor)); cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1); }
  const todayInRange = today >= minDate && today <= maxDate;
  return (
    <div className="mb-3">
      <div className="overflow-x-auto border border-[var(--leon-line)] rounded-xl bg-white">
        <div style={{ minWidth: 900 }}>
          <div className="flex border-b border-[var(--leon-line)]">
            <div className="shrink-0 border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} />
            <div className="relative flex-1 h-8">
              {months.map(m => (
                <div key={m} className="absolute top-0 h-full flex items-center text-[11px] font-bold text-[var(--leon-black)]/60 uppercase border-l border-[var(--leon-line)] pl-1.5" style={{ left: `${pctFor(m)}%` }}>
                  {fromISO(m).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })}
                </div>
              ))}
            </div>
          </div>
          <div className="relative">
            <div className="absolute inset-0 pointer-events-none" style={{ left: LABEL_W }}>
              {months.map(m => <div key={m} className="absolute top-0 bottom-0 border-l border-[var(--leon-line)]" style={{ left: `${pctFor(m)}%` }} />)}
              {todayInRange && <div className="absolute top-0 bottom-0 w-0.5 bg-[var(--leon-red)]" style={{ left: `${pctFor(today)}%` }} />}
            </div>
            {ws.nodes.map((n, i) => {
              const status = deriveWindowScheduleStatus(n, today);
              const color = WINDOW_SCHEDULE_STATUS_COLORS[status];
              const isCritical = criticalPath.has(n.id);
              const hasDates = !!(n.forecastStart && n.forecastEnd);
              const leftPct = hasDates ? pctFor(n.forecastStart) : 0;
              const widthPct = hasDates ? Math.max(0.6, pctFor(n.forecastEnd) - leftPct) : 0;
              const baseLeft = n.baselineStart ? pctFor(n.baselineStart) : null;
              const baseWidth = (n.baselineStart && n.baselineEnd) ? Math.max(0.6, pctFor(n.baselineEnd) - baseLeft) : null;
              return (
                <div key={n.id} className="flex items-center" style={{ background: i % 2 ? 'var(--leon-cream)' : 'transparent' }}>
                  <div className="shrink-0 pl-3 pr-3 py-1 text-xs text-[var(--leon-black)]/80 truncate border-r border-[var(--leon-line)]" style={{ width: LABEL_W }} title={n.name}>{n.name}</div>
                  <div className="relative flex-1" style={{ height: 34 }}>
                    {baseLeft !== null && (
                      <div className="absolute rounded" style={{ left: `${baseLeft}%`, width: `${baseWidth}%`, top: 4, height: 8, background: 'var(--leon-black)', opacity: 0.15 }} title={`Baseline: ${fmtDate(n.baselineStart)} – ${fmtDate(n.baselineEnd)}`} />
                    )}
                    {hasDates && (
                      <div className="absolute rounded-full transition-transform hover:scale-y-125 hover:z-10" style={{ left: `${leftPct}%`, width: `${widthPct}%`, top: 14, height: 16, minWidth: 8, background: scheduleGradient(status), boxSizing: 'border-box', border: isCritical ? '2px solid var(--leon-black)' : 'none', boxShadow: '0 1px 3px rgba(22,19,17,.18)' }}
                        title={`${n.name}: ${fmtDate(n.forecastStart)} – ${fmtDate(n.forecastEnd)} (${status})${isCritical ? ' · Critical Path' : ''}`} />
                    )}
                    {n.actualEnd && (
                      <div className="absolute rounded-full" style={{ left: `${pctFor(n.actualEnd)}%`, top: 11, width: 6, height: 20, background: 'var(--leon-black)' }} title={`Actual complete: ${fmtDate(n.actualEnd)}`} />
                    )}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
      <div className="flex items-center gap-3 px-1 py-2 text-[11px] text-[var(--leon-black)]/60 flex-wrap">
        {Object.keys(WINDOW_SCHEDULE_STATUS_COLORS).map(s => (
          <span key={s} className="flex items-center gap-1.5"><span className="w-3 h-3 rounded-full" style={{ background: scheduleGradient(s) }} /> {s}</span>
        ))}
        <span className="flex items-center gap-1.5"><span className="w-3 h-1.5 rounded" style={{ background: 'var(--leon-black)', opacity: 0.15 }} /> Baseline</span>
        <span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded" style={{ border: '2px solid var(--leon-black)' }} /> Critical Path</span>
        <span className="flex items-center gap-1.5"><span className="w-0.5 h-3 bg-[var(--leon-red)] inline-block" /> Today</span>
      </div>
    </div>
  );
}

const WINDOW_APPROVAL_LABELS = { profileSystem: 'Profile/System Approval', glass: 'Glass Approval', fabricationDrawing: 'Fabrication Drawing Approval' };
function WindowScheduleSection({ ctx, project, scope, editable }) {
  const ws = scope.windowSchedule;
  const [logRevOpen, setLogRevOpen] = useState(false);
  const criticalPath = new Set(identifyCriticalPath(ws));
  return (
    <div className="mt-4 border-t border-[var(--leon-line)] pt-4">
      <div className="flex items-center justify-between flex-wrap gap-2 mb-3">
        <div>
          <p className="text-sm font-bold">🪟 Window Schedule — {ws.systemName || 'No system selected'}</p>
          <p className="text-xs text-[var(--leon-black)]/50">{ws.finish ? `Finish: ${ws.finish}` : ''}{ws.finish && ws.glass ? ' · ' : ''}{ws.glass ? `Glass: ${ws.glass}` : ''}</p>
        </div>
        {editable && <Button size="sm" variant="ghost" onClick={() => setLogRevOpen(true)}>+ Log Revision</Button>}
      </div>

      <div className="grid md:grid-cols-3 gap-2 mb-3">
        {Object.keys(WINDOW_APPROVAL_LABELS).map(key => (
          <WindowApprovalTrackerCard key={key} ctx={ctx} project={project} scope={scope} approvalKey={key} label={WINDOW_APPROVAL_LABELS[key]} editable={editable} />
        ))}
      </div>

      <WindowScheduleGanttView ws={ws} criticalPath={criticalPath} />

      <WindowLinkedRecordsPanel ctx={ctx} project={project} scope={scope} editable={editable} />
      <WindowDeliveryPhasesPanel ctx={ctx} project={project} scope={scope} editable={editable} />

      {ws.revisions.length > 0 && (
        <Collapsible title="Window Schedule Revisions" count={ws.revisions.length}>
          {[...ws.revisions].sort((a, b) => b.revisionNumber - a.revisionNumber).map(rev => (
            <div key={rev.id} className="text-xs border-t border-[var(--leon-line)] py-1.5 first:border-t-0">
              <div className="flex items-center gap-1.5 flex-wrap">
                <Badge tone={rev.impact === 'Change Order Required' ? 'red' : rev.impact === 'Major' ? 'amber' : 'neutral'}>Rev {rev.revisionNumber} · {rev.impact}</Badge>
                <span className="text-[var(--leon-black)]/40">{fmtDate(rev.date)} · {rev.loggedBy}</span>
                {rev.changeOrderId && <Badge tone="brown">Linked CO</Badge>}
              </div>
              <p className="text-[var(--leon-black)]/60 mt-0.5">{rev.description}</p>
            </div>
          ))}
        </Collapsible>
      )}
      <LogWindowRevisionModal open={logRevOpen} onClose={() => setLogRevOpen(false)} ctx={ctx} project={project} scope={scope} />
    </div>
  );
}

// Link picker for one node's relevant record type — a node never owns cost/
// vendor/status data, it only holds ids into the real Procurement/
// Production/Export record, which every existing module's own UI still
// fully owns (§ Phase 6 — integrate, don't duplicate).
function WindowNodeLinksPanel({ ctx, project, scope, node, editable }) {
  if (node.key === 'profile_approval' || node.key === 'glass_approval') {
    const estimates = project.vendorEstimates.filter(ve => ve.scopeId === scope.id);
    return (
      <div className="text-[11px] mt-1">
        <span className="text-[var(--leon-black)]/40">Linked Procurement: </span>
        {estimates.length === 0 ? <span className="text-[var(--leon-black)]/30 italic">none in Procurement Hub yet</span> : estimates.map(ve => (
          <label key={ve.id} className="inline-flex items-center gap-1 mr-2">
            <input type="checkbox" disabled={!editable} checked={node.linkedRecords.vendorEstimateIds.includes(ve.id)} onChange={() => ctx.linkWindowNodeRecord(project.id, scope.id, node.id, 'vendorEstimateIds', ve.id)} />
            {ve.category} — {fmtMoney(ve.amount)}
          </label>
        ))}
      </div>
    );
  }
  if (node.key === 'factory_first_delivery' || node.key === 'factory_full_production') {
    const records = project.productionRecords.filter(r => r.scopeId === scope.id);
    return (
      <div className="text-[11px] mt-1 flex items-center gap-1.5">
        <span className="text-[var(--leon-black)]/40">Linked Production:</span>
        {records.length === 0 ? <span className="text-[var(--leon-black)]/30 italic">none in Production Hub yet</span> : (
          <Select value={node.linkedRecords.productionRecordId || ''} onChange={e => ctx.linkWindowNodeRecord(project.id, scope.id, node.id, 'productionRecordId', e.target.value || null)} className="!inline-block !w-auto !py-0.5 !text-xs" disabled={!editable}>
            <option value="">— none —</option>
            {records.map(r => <option key={r.id} value={r.id}>{r.vendorName} — {r.status}</option>)}
          </Select>
        )}
      </div>
    );
  }
  if (node.key === 'shipping') {
    const containers = containersForProjectScope(ctx.exportContainers, project.id, scope.id);
    return (
      <div className="text-[11px] mt-1">
        <span className="text-[var(--leon-black)]/40">Linked Containers: </span>
        {containers.length === 0 ? <span className="text-[var(--leon-black)]/30 italic">none in Export Hub yet</span> : containers.map(c => (
          <label key={c.id} className="inline-flex items-center gap-1 mr-2">
            <input type="checkbox" disabled={!editable} checked={node.linkedRecords.exportContainerIds.includes(c.id)} onChange={() => ctx.linkWindowNodeRecord(project.id, scope.id, node.id, 'exportContainerIds', c.id)} />
            {c.containerNumber}
          </label>
        ))}
      </div>
    );
  }
  return null;
}

const WINDOW_LINKABLE_NODE_KEYS = ['profile_approval', 'glass_approval', 'factory_first_delivery', 'factory_full_production', 'shipping'];
function WindowLinkedRecordsPanel({ ctx, project, scope, editable }) {
  const relevant = scope.windowSchedule.nodes.filter(n => WINDOW_LINKABLE_NODE_KEYS.includes(n.key));
  return (
    <Collapsible title="Linked Records" count={relevant.length}>
      {relevant.map(n => (
        <div key={n.id} className="border-t border-[var(--leon-line)] py-1.5 first:border-t-0">
          <p className="text-xs font-semibold">{n.name}</p>
          <WindowNodeLinksPanel ctx={ctx} project={project} scope={scope} node={n} editable={editable} />
        </div>
      ))}
    </Collapsible>
  );
}

// Unlimited phased/building delivery tracking (§ requirement #7) — each
// phase is a real DAG node (dependent on Shipping) AND links to real
// Delivery Hub / Export Hub records, never freehand data of its own.
function WindowDeliveryPhasesPanel({ ctx, project, scope, editable }) {
  const ws = scope.windowSchedule;
  const [showAdd, setShowAdd] = useState(false);
  const [newName, setNewName] = useState('');
  const deliveries = project.deliveries.filter(d => d.scopeId === scope.id);
  const containers = containersForProjectScope(ctx.exportContainers, project.id, scope.id);
  return (
    <Collapsible title="Delivery Phases" count={ws.deliveryPhases.length} right={editable && <Button size="sm" variant="ghost" onClick={e => { e.stopPropagation(); setShowAdd(s => !s); }}>+ Add Phase</Button>}>
      {showAdd && (
        <div className="flex items-center gap-2 mb-2">
          <TextInput value={newName} onChange={e => setNewName(e.target.value)} placeholder="e.g. Building A" className="!w-48 !py-1 !text-xs" />
          <Button size="sm" onClick={() => { if (newName.trim()) { ctx.addWindowDeliveryPhase(project.id, scope.id, newName.trim()); setNewName(''); setShowAdd(false); } }}>Save</Button>
        </div>
      )}
      {ws.deliveryPhases.length === 0 ? <EmptyState text="No delivery phases yet." /> : ws.deliveryPhases.map(phase => {
        const node = ws.nodes.find(n => n.id === phase.nodeId);
        return (
          <div key={phase.id} className="border-t border-[var(--leon-line)] py-1.5 first:border-t-0 text-xs">
            <div className="flex items-center gap-1.5 flex-wrap mb-1">
              <span className="font-semibold">{phase.name}</span>
              {node && node.forecastEnd && <Badge tone="neutral">Fcst {fmtDate(node.forecastEnd)}</Badge>}
            </div>
            <div className="mb-0.5">
              <span className="text-[var(--leon-black)]/40">Deliveries: </span>
              {deliveries.length === 0 ? <span className="text-[var(--leon-black)]/30 italic">none in Delivery Hub yet</span> : deliveries.map(d => (
                <label key={d.id} className="inline-flex items-center gap-1 mr-2">
                  <input type="checkbox" disabled={!editable} checked={phase.deliveryIds.includes(d.id)} onChange={() => ctx.linkWindowPhaseRecord(project.id, scope.id, phase.id, 'deliveryIds', d.id)} />
                  {d.deliveryNumber}
                </label>
              ))}
            </div>
            <div>
              <span className="text-[var(--leon-black)]/40">Containers: </span>
              {containers.length === 0 ? <span className="text-[var(--leon-black)]/30 italic">none in Export Hub yet</span> : containers.map(c => (
                <label key={c.id} className="inline-flex items-center gap-1 mr-2">
                  <input type="checkbox" disabled={!editable} checked={phase.exportContainerIds.includes(c.id)} onChange={() => ctx.linkWindowPhaseRecord(project.id, scope.id, phase.id, 'exportContainerIds', c.id)} />
                  {c.containerNumber}
                </label>
              ))}
            </div>
          </div>
        );
      })}
    </Collapsible>
  );
}

function WindowApprovalTrackerCard({ ctx, project, scope, approvalKey, label, editable }) {
  const ws = scope.windowSchedule;
  const approval = ws.approvals[approvalKey];
  const node = ws.nodes.find(n => n.id === approval.nodeId);
  const thread = approval.submittalThreadId ? scope.submittals.find(t => t.id === approval.submittalThreadId) : null;
  const [startOpen, setStartOpen] = useState(false);
  const latestRev = thread && thread.revisions.length ? [...thread.revisions].sort((a, b) => b.revisionNumber - a.revisionNumber)[0] : null;
  const isApproved = thread && ['Approved', 'Approved as Noted'].includes(thread.status);
  const isLate = approval.requiredByDate && !isApproved && latestRev && latestRev.date > approval.requiredByDate;
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-2.5 bg-white">
      <div className="flex items-center justify-between gap-1.5 mb-1.5 flex-wrap">
        <p className="text-xs font-bold">{label}</p>
        <div className="flex items-center gap-1 flex-wrap">
          {node && node.forecastEnd && <Badge tone="neutral">Fcst {fmtDate(node.forecastEnd)}</Badge>}
          {approval.requiredByDate && <Badge tone={isLate ? 'red' : 'neutral'}>Req. {fmtDate(approval.requiredByDate)}</Badge>}
          {isApproved ? <Badge tone="green">Approved</Badge> : isLate ? <Badge tone="red">Late</Badge> : null}
        </div>
      </div>
      {thread ? (
        <SubmittalThreadCard ctx={ctx} project={project} scope={scope} thread={thread} isResponse={false} editable={editable} />
      ) : (
        editable ? <Button size="sm" variant="ghost" onClick={() => setStartOpen(true)}>+ Start Tracking</Button> : <p className="text-xs text-[var(--leon-black)]/40">Not started.</p>
      )}
      <StartWindowApprovalModal open={startOpen} onClose={() => setStartOpen(false)} ctx={ctx} project={project} scope={scope} approvalKey={approvalKey} label={label} />
    </div>
  );
}

function StartWindowApprovalModal({ open, onClose, ctx, project, scope, approvalKey, label }) {
  const blank = { name: label, vendorId: '', date: todayISO(), status: 'Submitted', requiredByDate: '', file: '', fileUrl: null, notes: '', responsiblePerson: ctx.currentUserName };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, name: label, responsiblePerson: ctx.currentUserName }); }, [open, label]);
  function submit() {
    if (!form.name.trim() || !form.file) return;
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    ctx.addWindowApprovalThread(project.id, scope.id, approvalKey, { ...form, vendorName: vendor ? vendor.name : '', requiredByDate: form.requiredByDate || null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Start Tracking — ${label}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Start Tracking</Button></>}>
      <div className="space-y-3">
        <Field label="Document Name / Number"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Vendor / Manufacturer"><Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}><option value="">—</option>{ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</Select></Field>
          <Field label="Submission Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{SUBMITTAL_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
          <Field label="Required By" hint="Compared against each revision's date to flag late approvals."><TextInput type="date" value={form.requiredByDate} onChange={e => setForm({ ...form, requiredByDate: e.target.value })} /></Field>
        </div>
        <Field label="Attachment (Rev. 0)"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

const WINDOW_REVISION_IMPACTS = ['No Impact', 'Minor', 'Major', 'Change Order Required'];
function LogWindowRevisionModal({ open, onClose, ctx, project, scope }) {
  const blank = { description: '', impact: 'No Impact', materialAffected: false, procurementNeeded: false, coType: 'Change Order', coAmount: '', coDate: todayISO(), coDescription: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.description.trim()) return;
    const coData = form.impact === 'Change Order Required'
      ? { type: form.coType, amount: Number(form.coAmount) || 0, date: form.coDate, description: form.coDescription || form.description }
      : null;
    ctx.logWindowRevision(project.id, scope.id, { description: form.description, impact: form.impact, materialAffected: form.materialAffected, procurementNeeded: form.procurementNeeded, coData });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Log Window Schedule Revision — ${scope.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Log Revision</Button></>}>
      <div className="space-y-3">
        <Field label="What changed"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="e.g. Client changed glass spec from Triple Pane to Impact-Rated" /></Field>
        <Field label="Impact" hint="Change Order Required creates a real entry in this project's Change Orders.">
          <Select value={form.impact} onChange={e => setForm({ ...form, impact: e.target.value })}>{WINDOW_REVISION_IMPACTS.map(i => <option key={i}>{i}</option>)}</Select>
        </Field>
        {(form.impact === 'Minor' || form.impact === 'Major' || form.impact === 'Change Order Required') && (
          <div className="flex items-center gap-4">
            <label className="flex items-center gap-1.5 text-xs"><input type="checkbox" checked={form.materialAffected} onChange={e => setForm({ ...form, materialAffected: e.target.checked })} /> Material affected</label>
            <label className="flex items-center gap-1.5 text-xs"><input type="checkbox" checked={form.procurementNeeded} onChange={e => setForm({ ...form, procurementNeeded: e.target.checked })} /> New procurement needed</label>
          </div>
        )}
        {form.impact === 'Change Order Required' && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-[var(--leon-cream)]/40 space-y-3">
            <p className="text-xs text-[var(--leon-black)]/50">Change Order details</p>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Type"><Select value={form.coType} onChange={e => setForm({ ...form, coType: e.target.value })}><option>Change Order</option><option>Back Charge</option></Select></Field>
              <Field label="Amount"><TextInput type="number" min="0" value={form.coAmount} onChange={e => setForm({ ...form, coAmount: e.target.value })} /></Field>
            </div>
            <Field label="Date"><TextInput type="date" value={form.coDate} onChange={e => setForm({ ...form, coDate: e.target.value })} /></Field>
            <Field label="CO Description"><TextArea rows={2} value={form.coDescription} onChange={e => setForm({ ...form, coDescription: e.target.value })} placeholder="Defaults to the revision description above if left blank." /></Field>
          </div>
        )}
      </div>
    </Modal>
  );
}

function ReportDelayModal({ open, stage, onClose, onSubmit }) {
  const [days, setDays] = useState(1);
  const [reason, setReason] = useState(DELAY_REASON_CATEGORIES[0]);
  const [note, setNote] = useState('');
  useEffect(() => { if (open) { setDays(1); setReason(DELAY_REASON_CATEGORIES[0]); setNote(''); } }, [open]);
  if (!stage) return null;
  return (
    <Modal open={open} onClose={onClose} title={`Report Delay — ${stage.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={() => onSubmit(Number(days), reason, note)}>Apply Delay & Cascade</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">This shifts the planned start/due of every downstream stage in this scope by the same number of days, and recalculates the scope's projected completion and the project's health indicator.</p>
        <Field label="Delay (business days)"><TextInput type="number" min="1" value={days} onChange={e => setDays(e.target.value)} /></Field>
        <Field label="Reason Category">
          <Select value={reason} onChange={e => setReason(e.target.value)}>
            {DELAY_REASON_CATEGORIES.map(r => <option key={r} value={r}>{r}</option>)}
          </Select>
        </Field>
        <Field label="Note"><TextArea rows={3} value={note} onChange={e => setNote(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}

function AddScopeModal({ open, onClose, ctx, project }) {
  const [name, setName] = useState('');
  const [familyName, setFamilyName] = useState(ctx.scopeLibrary[0]?.name || '');
  const [leadTimeMode, setLeadTimeMode] = useState('start'); // 'start' | 'jobsite'
  const [startDate, setStartDate] = useState(todayISO());
  const [jobsiteDate, setJobsiteDate] = useState(todayISO());
  const [windowSystemId, setWindowSystemId] = useState('');
  const [windowFinish, setWindowFinish] = useState('');
  const [windowGlass, setWindowGlass] = useState('');
  const [scopeType, setScopeType] = useState(DEFAULT_SCOPE_TYPE);
  useEffect(() => { if (open) { setName(''); setFamilyName(ctx.scopeLibrary.find(f => f.active)?.name || ''); setLeadTimeMode('start'); setStartDate(todayISO()); setJobsiteDate(todayISO()); setWindowSystemId(''); setWindowFinish(''); setWindowGlass(''); setScopeType(DEFAULT_SCOPE_TYPE); } }, [open]);
  const computedStartDate = leadTimeMode === 'jobsite' ? startDateForJobsiteDate(jobsiteDate, project.complexity) : startDate;
  const isWindowFamily = isWindowSystemFamily(familyName, ctx.scopeLibrary);
  const isCountertopFamilyPick = isCountertopFamily(familyName, ctx.scopeLibrary);
  const typeOptions = scopeTypeOptions(familyName, ctx.scopeLibrary);
  const effectiveType = effectiveScopeType(familyName, ctx.scopeLibrary, scopeType);
  const previewStages = stageDefsForScope(familyName, ctx.scopeLibrary, ctx.interiorLeadTimeLibrary, effectiveType);
  const windowFamily = ctx.scopeLibrary.find(f => f.name === familyName);
  const finishOptions = windowFamily?.categories.find(c => c.name === 'Finish Color')?.options.filter(o => o.active) || [];
  const glassOptions = windowFamily?.categories.find(c => c.name === 'Glazing Type')?.options.filter(o => o.active) || [];
  function submit() {
    if (!name.trim() || !familyName) return;
    ctx.addScope(project.id, { name, familyName, scopeType: effectiveType, startDate: computedStartDate, windowSystemId: isWindowFamily ? (windowSystemId || null) : null, windowFinish: isWindowFamily ? (windowFinish || null) : null, windowGlass: isWindowFamily ? (windowGlass || null) : null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Add Scope" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Scope</Button></>}>
      <div className="space-y-3">
        {/* Supply Only vs Supply & Install. This is a property of the SCOPE,
            not the family — the same casework can be sold either way on
            different jobs — and it decides whether the scope has an
            installation phase at all. */}
        {/* Supply and labour are contracted and billed separately, so they are
            two scopes, not one scope with a longer stage list. Countertops are
            the one package — the same contract buys the slab and installs it. */}
        <Field label="What are we doing on this scope?" hint={isCountertopFamilyPick
          ? 'Countertops are the one package: one contract buys the slab, templates it and installs it, so there is nothing to split here.'
          : 'Supply and labour are contracted and billed separately, so they are separate scopes. Add one of each when LEON is doing both.'}>
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit flex-wrap">
            {typeOptions.map(t => (
              <button key={t} type="button" disabled={typeOptions.length === 1} onClick={() => setScopeType(t)}
                className={`px-3 py-1.5 rounded-md text-sm font-semibold disabled:opacity-100 ${effectiveType === t ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>
                {t === 'Supply Only' ? '📦 Supply Only' : t === 'Labor Only' ? '🛠️ Labor Only' : '🪨 Supply & Labor'}
              </button>
            ))}
          </div>
        </Field>
        {/* What this actually produces — the schedule is the thing being
            chosen here, so it is worth showing before the scope exists. */}
        <p className="text-[11px] text-[var(--leon-black)]/45 -mt-1">
          <b>{previewStages.length} stages:</b> {previewStages.map(d => d.name).join(' → ')}
        </p>
        <Field label="Scope Name" hint="Standard names for this family are suggested — type anything else for a custom scope.">
          <TextInput value={name} onChange={e => setName(e.target.value)} placeholder="e.g. Kitchen Cabinetry" list="scope-name-suggestions" />
          <datalist id="scope-name-suggestions">
            {(SUGGESTED_SCOPE_NAMES[familyName] || []).map(n => <option key={n} value={n} />)}
          </datalist>
        </Field>
        <Field label="Scope Family">
          <Select value={familyName} onChange={e => setFamilyName(e.target.value)}>
            {ctx.scopeLibrary.filter(f => f.active && !FLOORING_FAMILY_NAMES.includes(f.name)).map(f => <option key={f.id} value={f.name}>{f.name}</option>)}
            {ctx.scopeLibrary.some(f => f.active && FLOORING_FAMILY_NAMES.includes(f.name)) && (
              <optgroup label="Flooring">
                {ctx.scopeLibrary.filter(f => f.active && FLOORING_FAMILY_NAMES.includes(f.name)).map(f => <option key={f.id} value={f.name}>{f.name}</option>)}
              </optgroup>
            )}
          </Select>
        </Field>
        {isWindowFamily && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 bg-[var(--leon-cream)]/40 space-y-3">
            <p className="text-xs text-[var(--leon-black)]/50">This family uses the specialized Window/Exterior Door System schedule — pick a system to pull its default lead times from the Window System Lead-Time Library (LEON Collection).</p>
            <Field label="System">
              <Select value={windowSystemId} onChange={e => setWindowSystemId(e.target.value)}>
                <option value="">— none —</option>
                {ctx.windowLeadTimeLibrary.filter(e => e.active).map(e => <option key={e.id} value={e.id}>{e.name}</option>)}
              </Select>
            </Field>
            <Field label="Finish">
              {finishOptions.length > 0 ? (
                <Select value={windowFinish} onChange={e => setWindowFinish(e.target.value)}>
                  <option value="">— none —</option>
                  {finishOptions.map(o => <option key={o.id} value={o.name}>{o.name}</option>)}
                </Select>
              ) : <TextInput value={windowFinish} onChange={e => setWindowFinish(e.target.value)} placeholder="e.g. Black Anodized" />}
            </Field>
            <Field label="Glass">
              {glassOptions.length > 0 ? (
                <Select value={windowGlass} onChange={e => setWindowGlass(e.target.value)}>
                  <option value="">— none —</option>
                  {glassOptions.map(o => <option key={o.id} value={o.name}>{o.name}</option>)}
                </Select>
              ) : <TextInput value={windowGlass} onChange={e => setWindowGlass(e.target.value)} placeholder="e.g. Triple Pane" />}
            </Field>
          </div>
        )}
        <Field label="Lead Time">
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
            {[{ key: 'start', label: 'Start Date' }, { key: 'jobsite', label: 'Jobsite Date' }].map(o => (
              <button key={o.key} type="button" onClick={() => setLeadTimeMode(o.key)} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-colors ${leadTimeMode === o.key ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}>{o.label}</button>
            ))}
          </div>
        </Field>
        {leadTimeMode === 'start' ? (
          <Field label="Schedule Start Date" hint={`Generates the ${previewStages.length} stages above from this date, using the project's complexity multiplier.`}><TextInput type="date" value={startDate} onChange={e => setStartDate(e.target.value)} /></Field>
        ) : (
          <Field label="Jobsite Date" hint="The date material needs to be delivered to the jobsite — the schedule start date is backdated from here so Delivery to Jobsite lands on this date.">
            <TextInput type="date" value={jobsiteDate} onChange={e => setJobsiteDate(e.target.value)} />
            <p className="text-xs text-[var(--leon-black)]/50 mt-1">Schedule would start <strong>{fmtDate(computedStartDate)}</strong>.</p>
          </Field>
        )}
      </div>
    </Modal>
  );
}

// ---- Selections -------------------------------------------------------------
// Shared by every "Hub" that gets per-scope subtabs (Selection, Shop
// Drawing, Procurement, Production, Export) — Overview keeps rendering the
// hub's existing full content completely unchanged (zero regression risk);
// each scope gets its own tab showing just that scope's slice via
// `renderScope`. Scope tabs appear automatically as scopes are added — no
// manual subtab management.
function ScopeHubTabs({ ctx, project, overview, renderScope }) {
  const [sub, setSub] = useState('overview');
  // Department-scoped like ScopesTab — ctx is optional so any caller that
  // hasn't been threaded through yet simply shows every scope as before.
  const visibleScopes = ctx ? ctx.deptScopes(project) : project.scopes;
  const tabs = [{ key: 'overview', label: 'Overview' }, ...visibleScopes.map(s => ({ key: s.id, label: s.name }))];
  const scope = sub !== 'overview' ? visibleScopes.find(s => s.id === sub) : null;
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}
            {t.label}
          </button>
        ))}
      </div>
      <HubTools />
      {sub === 'overview' ? overview : (scope ? renderScope(scope) : <EmptyState text="Scope not found." />)}
    </div>
  );
}
function SelectionsTab({ ctx, project, account }) {
  const editable = ctx.canEdit('selections');
  // Starts unarmed — nothing is pre-selected to print all scopes by default;
  // printing only happens once the user explicitly picks a scope's own
  // "Print Submittal" or the separate all-scopes Selection Sheet button.
  const [printMode, setPrintMode] = useState(null); // null | 'all' | scopeId (submittal)

  function printAll() { setPrintMode('all'); setTimeout(() => window.print(), 50); }
  function printSubmittal(scopeId) { setPrintMode(scopeId); setTimeout(() => window.print(), 50); }

  const printScope = printMode === 'all' ? null : project.scopes.find(s => s.id === printMode);

  const overview = (
    <div>
      <div className="no-print flex justify-end gap-2 mb-2">
        <ShareButton ctx={ctx} projectId={project.id} subjectKey={`selections:${project.id}`}
          subject={`Selections — ${project.name}`}
          summary={`${project.scopes.length} scope${project.scopes.length === 1 ? '' : 's'}`}
          items={project.scopes.map(sc => ({
            id: sc.id, label: sc.name,
            sub: `${(sc.selectionGroups || []).length || 1} area${((sc.selectionGroups || []).length || 1) === 1 ? '' : 's'}${sc.selectionsLocked ? ' · locked' : ''}`,
          }))} />
        <PrintButton onClick={printAll} label="Print Selection Sheet (all scopes)" />
      </div>
      <div className="no-print">
        {project.scopes.length === 0 ? <EmptyState text="No scopes yet." /> : project.scopes.map(scope => (
          <SelectionScopeBlock key={scope.id} ctx={ctx} project={project} scope={scope} editable={editable} onPrintSubmittal={() => printSubmittal(scope.id)} />
        ))}
      </div>

      <div className="print-only print-area p-8">
        {printMode === 'all' ? (
          <>
            <PrintHeader project={project} account={account} title="Selection Sheet" />
            {project.scopes.map(scope => {
              const family = ctx.scopeLibrary.find(f => f.name === scope.familyName);
              function SelectionTable({ selections }) {
                return (
                  <table className="w-full text-sm">
                    <tbody>
                      {family && family.categories.map(cat => {
                        const opt = cat.options.find(o => o.id === selections[cat.id]);
                        return (
                          <tr key={cat.id} className="border-b border-gray-200">
                            <td className="py-1 pr-3 w-14">{opt && opt.imageUrl && <img src={opt.imageUrl} className="w-10 h-10 object-cover rounded" />}</td>
                            <td className="py-1 pr-4 font-semibold w-1/3">{cat.name}</td>
                            <td className="py-1">{opt ? opt.name : '—'}</td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                );
              }
              return (
                <div key={scope.id} className="mb-5 break-inside-avoid">
                  <h3 className="font-bold text-base border-b border-black pb-1 mb-2">{scope.name} <span className="font-normal text-sm">({scope.familyName})</span></h3>
                  <SelectionTable selections={scope.selections} />
                  {scope.selectionAreas.map(area => (
                    <div key={area.id} className="mt-2">
                      <p className="text-xs font-bold uppercase text-gray-500 mt-2 mb-1">{area.name}</p>
                      <SelectionTable selections={area.selections} />
                    </div>
                  ))}
                </div>
              );
            })}
          </>
        ) : printScope ? (
          <SubmittalPrintBlock ctx={ctx} project={project} account={account} scope={printScope} />
        ) : null}
      </div>
    </div>
  );

  return <ScopeHubTabs ctx={ctx} project={project} overview={overview} renderScope={scope => (
    <div>
      <div className="no-print flex justify-end mb-2"><PrintButton onClick={() => printSubmittal(scope.id)} label="Print Submittal" /></div>
      <div className="no-print">
        <SelectionScopeBlock ctx={ctx} project={project} scope={scope} editable={editable} onPrintSubmittal={() => printSubmittal(scope.id)} />
      </div>
      <div className="print-only print-area p-8">
        {printScope && printScope.id === scope.id && <SubmittalPrintBlock ctx={ctx} project={project} account={account} scope={printScope} />}
      </div>
    </div>
  )} />;
}

function SubmittalPrintBlock({ ctx, project, account, scope }) {
  const family = ctx.scopeLibrary.find(f => f.name === scope.familyName);
  function SelectionGrid({ selections }) {
    return (
      <div className="grid grid-cols-2 gap-4">
        {family && family.categories.map(cat => {
          const opt = cat.options.find(o => o.id === selections[cat.id]);
          return (
            <div key={cat.id} className="border border-gray-300 rounded p-3 flex gap-3 items-center break-inside-avoid">
              {opt && opt.imageUrl ? (
                <img src={opt.imageUrl} className="w-20 h-20 object-cover rounded shrink-0" />
              ) : (
                <div className="w-20 h-20 bg-gray-100 flex items-center justify-center text-gray-300 text-xs shrink-0 rounded">No image</div>
              )}
              <div className="min-w-0">
                <div className="text-xs text-gray-500 uppercase font-semibold">{cat.name}</div>
                <div className="font-semibold">{opt ? opt.name : '—'}</div>
                {(scope.supplierFinishes || {})[cat.id] && <div className="text-xs text-gray-700 mt-0.5">{scope.supplierFinishes[cat.id].name} &middot; {scope.supplierFinishes[cat.id].code}</div>}
              </div>
            </div>
          );
        })}
      </div>
    );
  }
  return (
    <>
      <PrintHeader project={project} account={account} title={`Selections Submittal — ${scope.name}`} />
      <p className="text-sm text-gray-600 mb-4">Scope Family: {scope.familyName}</p>
      <SelectionGrid selections={scope.selections} />
      {scope.selectionAreas.map(area => (
        <div key={area.id} className="mt-5 break-inside-avoid">
          <h4 className="font-bold text-sm border-b border-gray-300 pb-1 mb-2">{area.name}</h4>
          <SelectionGrid selections={area.selections} />
        </div>
      ))}
    </>
  );
}

// Supplier finish picker — two steps rather than one enormous dropdown.
// Pick the CONSTRUCTION first (Melamine, Laminate, Acrylic, Veneer...), then
// type the decor name; matches are drawn from that construction only. With
// 1,102 laminate decors in the catalog, a plain <select> is unusable, and
// type-ahead is how these are actually referred to on site ("MS 202", "Folkstone").
function SupplierFinishPicker({ ctx, project, scope, area, categoryId, disabled }) {
  // `area` omitted -> the scope's main selection group; supplied -> that one
  // additional area group. Both store the same small reference shape.
  const holder = area || scope;
  const current = (holder.supplierFinishes || {})[categoryId] || null;
  const [open, setOpen] = useState(false);
  // One combined value "<supplier>\u0000<category>" keeps the two-part choice
  // in a single <select> without a second dropdown.
  const [pick, setPick] = useState(() => current ? `${current.source || ''}\u0000${current.cat || ''}` : '');
  const [q, setQ] = useState('');
  const groups = useMemo(() => supplierGroups(), []);
  const [sup, cat] = pick ? pick.split('\u0000') : ['', ''];
  const results = useMemo(() => (cat ? searchSupplierFinishes(sup, cat, q, 40) : []), [sup, cat, q]);
  const total = useMemo(() => {
    if (!cat) return 0;
    const g = groups.find(x => x.key === sup);
    return g ? ((g.cats.find(c => c.cat === cat) || {}).count || 0) : 0;
  }, [sup, cat, groups]);

  function apply(rec) {
    if (area) ctx.setAreaSupplierFinish(project.id, scope.id, area.id, categoryId, rec);
    else ctx.setSupplierFinish(project.id, scope.id, categoryId, rec);
  }
  function choose(rec) { apply(rec); setOpen(false); setQ(''); }

  if (current && !open) {
    return (
      <div className="flex items-center gap-1.5">
        {current.img && <img src={current.img} alt="" className="w-8 h-8 object-cover rounded border border-[var(--leon-line)]" />}
        <div className="min-w-0 leading-tight">
          <div className="text-xs font-semibold truncate max-w-[150px]" title={`${current.name} — ${current.code}`}>{current.name}</div>
          <div className="text-[10px] text-[var(--leon-black)]/45 truncate max-w-[150px]">{current.code}{` · ${supplierDisplayName(current.source, ctx.vendors)}`}</div>
        </div>
        {!disabled && (
          <>
            <button onClick={() => { setPick(`${current.source || ''}\u0000${current.cat || ''}`); setOpen(true); }} className="text-[11px] text-[var(--leon-brown)] font-semibold">Change</button>
            <IconBtn title="Remove supplier finish" onClick={() => apply(null)}>✕</IconBtn>
          </>
        )}
      </div>
    );
  }
  if (disabled) return null;
  if (!open) {
    return <button onClick={() => setOpen(true)} className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap">+ Supplier finish</button>;
  }
  return (
    <div className="border border-[var(--leon-brown)] rounded-lg p-2 bg-white w-full max-w-lg space-y-2">
      <div className="flex items-center gap-2">
        <Select value={pick} onChange={e => { setPick(e.target.value); setQ(''); }} className="!py-1 !text-xs !w-64">
          <option value="">Vendor &amp; construction…</option>
          {groups.map(g => (
            <optgroup key={g.key} label={supplierDisplayName(g.key, ctx.vendors)}>
              {g.cats.map(c => <option key={c.sup + c.cat} value={`${c.sup}\u0000${c.cat}`}>{c.cat} ({c.count})</option>)}
            </optgroup>
          ))}
        </Select>
        <TextInput autoFocus={!!cat} value={q} onChange={e => setQ(e.target.value)} disabled={!cat}
          placeholder={cat ? `Search ${total} in ${cat} — name or supplier code…` : 'Pick a vendor & category first'}
          className="!py-1 !text-xs flex-1" />
        <IconBtn title="Close" onClick={() => { setOpen(false); setQ(''); }}>✕</IconBtn>
      </div>
      {cat && (
        results.length === 0
          ? <p className="text-xs text-[var(--leon-black)]/45 px-1 py-2">No {cat} decor matches &ldquo;{q}&rdquo;.</p>
          : (
            <div className="max-h-64 overflow-y-auto divide-y divide-[var(--leon-line)]">
              {results.map(r => (
                <button key={r.id} onClick={() => choose(r)}
                  className="w-full flex items-center gap-2 px-1 py-1.5 text-left hover:bg-[var(--leon-cream)]">
                  <img src={r.img} alt="" loading="lazy" className="w-10 h-10 object-cover rounded border border-[var(--leon-line)] shrink-0" />
                  <span className="min-w-0">
                    <span className="block text-xs font-semibold truncate">{r.name}</span>
                    <span className="block text-[10px] text-[var(--leon-black)]/45 truncate">
                      {r.code}{r.color ? ` · ${r.color}` : ''}{r.surface ? ` · ${r.surface}` : ''}
                    </span>
                  </span>
                </button>
              ))}
            </div>
          )
      )}
      {cat && results.length >= 40 && <p className="text-[10px] text-[var(--leon-black)]/40 px-1">Showing the first 40 matches &mdash; keep typing to narrow.</p>}
    </div>
  );
}


// ── Locking selections, and the meeting that produced them ──────────────────
// Selections are often made LIVE — sitting with the client and entering what
// they choose on the spot. Locking is already the moment a numbered revision is
// captured, so it is also the right moment to record WHERE that happened and
// WHO was in the room. The meeting therefore rides on the revision rather than
// becoming a second record beside it.
//
// The meeting half is optional and off by default, because plenty of scopes are
// locked at a desk from an email. Asking every time and accepting "no" is
// better than a separate button nobody finds.
function LockSelectionsModal({ ctx, project, scope, onClose }) {
  const nextRevision = (scope.selectionRevisions && scope.selectionRevisions.length
    ? Math.max(...scope.selectionRevisions.map(r => r.revisionNumber)) : 0) + 1;
  const [held, setHeld] = useState(false);
  const [date, setDate] = useState(todayISO());
  const [location, setLocation] = useState('');
  const [clientPresent, setClientPresent] = useState(true);
  const [attendees, setAttendees] = useState(ctx.currentUserName || '');
  const [notes, setNotes] = useState('');

  // Somewhere to hold it. The job's own address is the common answer and the
  // showroom is the other, so both are one click rather than something to type.
  const quick = [
    project.address ? { label: 'Job site', value: project.address } : null,
    { label: 'LEON showroom', value: `${(ctx.companyProfile || {}).name || 'LEON'} showroom` },
    { label: 'Video call', value: 'Video call' },
  ].filter(Boolean);

  return (
    <Modal open title={`🔒 Lock selections — Revision ${nextRevision}`} onClose={onClose} size="lg">
      <div className="space-y-4">
        <p className="text-sm text-[var(--leon-black)]/65">
          Locking captures <b>Revision {nextRevision}</b> of {scope.name} &mdash; a snapshot of every finish
          chosen right now. Nothing can be changed afterwards until someone requests a revision.
        </p>

        <label className="flex items-start gap-2 rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/50 p-3 cursor-pointer">
          <input type="checkbox" checked={held} onChange={e => setHeld(e.target.checked)} className="mt-0.5" />
          <span>
            <span className="block text-sm font-semibold">These were chosen at a selection meeting</span>
            <span className="block text-xs text-[var(--leon-black)]/55">
              Tick this when the choices were made live with the client rather than settled by email. The
              date, the place and who was there are filed on the revision, so months later the record says
              how the finish was decided and not only what it is.
            </span>
          </span>
        </label>

        {held && (
          <div className="space-y-3 pl-1 border-l-2 border-[var(--leon-brown)]/30">
            <div className="grid sm:grid-cols-2 gap-3">
              <Field label="Meeting date">
                <TextInput type="date" value={date} onChange={e => setDate(e.target.value)} />
              </Field>
              <Field label="Was the client present?">
                <Select value={clientPresent ? 'yes' : 'no'} onChange={e => setClientPresent(e.target.value === 'yes')}>
                  <option value="yes">Yes — the client chose these in the room</option>
                  <option value="no">No — chosen on their behalf</option>
                </Select>
              </Field>
            </div>
            <Field label="Location" hint="Where the meeting was held.">
              <TextInput value={location} onChange={e => setLocation(e.target.value)}
                placeholder="e.g. 90 Revere Street, unit 3" />
              <span className="flex gap-1.5 flex-wrap mt-1.5">
                {quick.map(q => (
                  <button key={q.label} type="button" onClick={() => setLocation(q.value)}
                    className="text-[11px] px-2 py-0.5 rounded-full border border-[var(--leon-line)] hover:bg-[var(--leon-cream)]">
                    {q.label}
                  </button>
                ))}
              </span>
            </Field>
            <Field label="Who was there" hint="Free text — not everyone in the room has a Hub login.">
              <TextInput value={attendees} onChange={e => setAttendees(e.target.value)}
                placeholder="e.g. Ana LEON, the client, their designer" />
            </Field>
            <Field label="Notes" hint="What was discussed, what was ruled out, anything promised.">
              <TextArea rows={3} value={notes} onChange={e => setNotes(e.target.value)} />
            </Field>
            {!clientPresent && (
              <p className="text-xs text-[var(--leon-black)]/55">
                Recorded as chosen without the client. Worth saying in the notes who authorised it &mdash;
                that is the question asked later.
              </p>
            )}
          </div>
        )}

        <div className="flex justify-end gap-2 pt-1">
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          <Button onClick={() => {
            ctx.setScopeSelectionsLocked(project.id, scope.id, true,
              held ? { held: true, date, location, clientPresent, attendees, notes } : null);
            onClose();
          }}>🔒 Lock &mdash; Revision {nextRevision}</Button>
        </div>
      </div>
    </Modal>
  );
}

function SelectionScopeBlock({ ctx, project, scope, editable, onPrintSubmittal }) {
  const family = ctx.scopeLibrary.find(f => f.name === scope.familyName);
  const [addingCat, setAddingCat] = useState(false);
  const [newCatName, setNewCatName] = useState('');
  const [addingOptFor, setAddingOptFor] = useState(null);
  const [newOptName, setNewOptName] = useState('');
  const [addingArea, setAddingArea] = useState(false);
  const [newAreaName, setNewAreaName] = useState('');
  const [locking, setLocking] = useState(false);

  if (!family) return null;
  const visibleCats = family.categories.filter(c => c.active || scope.selections[c.id]);

  return (
    <>
    <Collapsible
      title={scope.name}
      count={visibleCats.length}
      right={
        <div className="flex items-center gap-2">
          <Badge tone="neutral">{scope.familyName}</Badge>
          {scope.selectionsLocked ? <Badge tone="green">🔒 Locked</Badge> : null}
          {editable && (
            scope.selectionsLocked
              ? <Button size="sm" variant="ghost" onClick={() => ctx.setScopeSelectionsLocked(project.id, scope.id, false)}>Request Revision</Button>
              : <Button size="sm" variant="ghost" onClick={e => { e.stopPropagation(); setLocking(true); }}>🔒 Lock Selections</Button>
          )}
          {/* This scope's selections, grouped by application area. */}
          <span onClick={e => e.stopPropagation()}>
            <ShareButton ctx={ctx} projectId={project.id} subjectKey={`scopeSelections:${scope.id}`}
              subject={`Selections — ${scope.name}`}
              summary={`${project.name}${scope.selectionsLocked ? ' · locked' : ''}`}
              items={scopeSelectionPackageItems(scope, ctx.scopeLibrary)} />
          </span>
          <Button size="sm" variant="ghost" onClick={onPrintSubmittal}>🖨 Print Submittal</Button>
        </div>
      }
    >
      {scope.selectionsLocked && <p className="text-xs text-[var(--leon-black)]/50 mb-2">Selections are locked. Use "Request Revision" above to reopen them for a change.</p>}
      {/* The application area this main group of selections covers — e.g.
          "Kitchen". Additional groups below are peers of it, each covering
          their own area, so one scope can carry several sets of finishes. */}
      <div className="flex items-center gap-2 mb-2 pb-2 border-b border-[var(--leon-line)]">
        <span className="text-[11px] font-bold uppercase tracking-wide text-[var(--leon-black)]/45 shrink-0">Application Area</span>
        {editable ? (
          <TextInput value={scope.mainAreaName || ''} onChange={e => ctx.setScopeMainAreaName(project.id, scope.id, e.target.value)}
            placeholder="e.g. Kitchen" className="!w-64 !py-1 !text-sm !font-semibold" />
        ) : (
          <span className="text-sm font-semibold">{scope.mainAreaName || <span className="text-[var(--leon-black)]/30 font-normal">Unnamed</span>}</span>
        )}
      </div>
      <div className="space-y-2">
        {visibleCats.map(cat => {
          const options = cat.options.filter(o => o.active || o.id === scope.selections[cat.id]);
          const selectedOpt = cat.options.find(o => o.id === scope.selections[cat.id]);
          return (
            <div key={cat.id} className="flex items-center gap-2 flex-wrap">
              <span className="text-sm w-40 shrink-0 text-[var(--leon-black)]/60">{cat.name}</span>
              {editable ? (
                <>
                  <Select disabled={scope.selectionsLocked} value={scope.selections[cat.id] || ''} onChange={e => ctx.setSelection(project.id, scope.id, cat.id, e.target.value)} className="!w-56">
                    <option value="">— none —</option>
                    {options.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
                  </Select>
                  {selectedOpt && (
                    <ImagePicker url={selectedOpt.imageUrl} onChange={url => ctx.lib.setOptionImage(selectedOpt.id, url)} size={32} />
                  )}
                  <SupplierFinishPicker ctx={ctx} project={project} scope={scope} categoryId={cat.id} disabled={scope.selectionsLocked} />
                  {addingOptFor === cat.id ? (
                    <div className="flex items-center gap-1">
                      <TextInput autoFocus value={newOptName} onChange={e => setNewOptName(e.target.value)} placeholder="New finish name" className="!w-40 !py-1 !text-xs" />
                      <Button size="sm" onClick={() => { if (newOptName.trim()) { ctx.lib.addOption(cat.id, newOptName.trim()); setNewOptName(''); setAddingOptFor(null); } }}>Add</Button>
                      <Button size="sm" variant="ghost" onClick={() => setAddingOptFor(null)}>✕</Button>
                    </div>
                  ) : (
                    <button onClick={() => setAddingOptFor(cat.id)} className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap">+ Add finish</button>
                  )}
                </>
              ) : (
                <>
                  {selectedOpt && selectedOpt.imageUrl && <img src={selectedOpt.imageUrl} className="w-8 h-8 object-cover rounded" />}
                  <span className="text-sm font-semibold">{selectedOpt ? selectedOpt.name : '—'}</span>
                  {(scope.supplierFinishes || {})[cat.id] && (
                    <span className="flex items-center gap-1.5">
                      {scope.supplierFinishes[cat.id].img && <img src={scope.supplierFinishes[cat.id].img} alt="" className="w-8 h-8 object-cover rounded border border-[var(--leon-line)]" />}
                      <span className="text-xs">{scope.supplierFinishes[cat.id].name} <span className="text-[var(--leon-black)]/45">{scope.supplierFinishes[cat.id].code}</span></span>
                    </span>
                  )}
                </>
              )}
              {/* The paperwork on file for this category — spec sheet,
                  warranty, care instructions. The material itself is the
                  supplier finish picked above; linking a second "material
                  spec" record only duplicated it. */}
              <CategoryDocsLink ctx={ctx} familyName={scope.familyName} categoryId={cat.id} />
            </div>
          );
        })}
        {editable && (
          addingCat ? (
            <div className="flex items-center gap-1 pt-1">
              <TextInput autoFocus value={newCatName} onChange={e => setNewCatName(e.target.value)} placeholder="New category name" className="!w-56 !py-1 !text-xs" />
              <Button size="sm" onClick={() => { if (newCatName.trim()) { ctx.lib.addCategory(family.id, newCatName.trim()); setNewCatName(''); setAddingCat(false); } }}>Add</Button>
              <Button size="sm" variant="ghost" onClick={() => setAddingCat(false)}>✕</Button>
            </div>
          ) : (
            <button onClick={() => setAddingCat(true)} className="text-xs text-[var(--leon-brown)] font-semibold pt-1">+ Add category</button>
          )
        )}

        {/* Named sub-areas within this scope — e.g. under a Tile scope,
            "Primary Bath Shower Surround" needs its own tile choice separate
            from the rest of the unit's floor tile. */}
        {(scope.selectionAreas.length > 0 || editable) && (
          <div className="mt-3 pt-3 border-t border-[var(--leon-line)]">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-2">Additional Application Areas</p>
            {scope.selectionAreas.map(area => (
              <div key={area.id} className="mb-3 pl-3 border-l-2 border-[var(--leon-line)]">
                <div className="flex items-center justify-between gap-2 mb-1.5">
                  {editable ? (
                    <TextInput value={area.name} onChange={e => ctx.renameSelectionArea(project.id, scope.id, area.id, e.target.value)}
                      placeholder="Area name" className="!w-64 !py-1 !text-sm !font-semibold" />
                  ) : (
                    <span className="text-sm font-semibold">{area.name}</span>
                  )}
                  {editable && <IconBtn title="Remove area" onClick={() => ctx.removeSelectionArea(project.id, scope.id, area.id)}>✕</IconBtn>}
                </div>
                <div className="space-y-2">
                  {visibleCats.map(cat => {
                    const options = cat.options.filter(o => o.active || o.id === area.selections[cat.id]);
                    const selectedOpt = cat.options.find(o => o.id === area.selections[cat.id]);
                    return (
                      <div key={cat.id} className="flex items-center gap-2 flex-wrap">
                        <span className="text-sm w-40 shrink-0 text-[var(--leon-black)]/60">{cat.name}</span>
                        {editable ? (
                          <Select value={area.selections[cat.id] || ''} onChange={e => ctx.setAreaSelection(project.id, scope.id, area.id, cat.id, e.target.value)} className="!w-56">
                            <option value="">— none —</option>
                            {options.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
                          </Select>
                        ) : (
                          <span className="text-sm font-semibold">{selectedOpt ? selectedOpt.name : '—'}</span>
                        )}
                        <SupplierFinishPicker ctx={ctx} project={project} scope={scope} area={area} categoryId={cat.id} disabled={!editable || scope.selectionsLocked} />
                      </div>
                    );
                  })}
                </div>
              </div>
            ))}
            {editable && (
              addingArea ? (
                <div className="flex items-center gap-1">
                  <TextInput autoFocus value={newAreaName} onChange={e => setNewAreaName(e.target.value)} placeholder="e.g. Base Cabinets, Upper Cabinets, Island" className="!w-64 !py-1 !text-xs" />
                  <Button size="sm" onClick={() => { if (newAreaName.trim()) { ctx.addSelectionArea(project.id, scope.id, newAreaName.trim()); setNewAreaName(''); setAddingArea(false); } }}>Add</Button>
                  <Button size="sm" variant="ghost" onClick={() => setAddingArea(false)}>✕</Button>
                </div>
              ) : (
                <button onClick={() => setAddingArea(true)} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Application Area</button>
              )
            )}
          </div>
        )}

        {scope.selectionRevisions && scope.selectionRevisions.length > 0 && (
          <div className="mt-3 pt-3 border-t border-[var(--leon-line)]">
            <Collapsible title="Revision History" count={scope.selectionRevisions.length}>
              <div className="space-y-1.5">
                {[...scope.selectionRevisions].sort((a, b) => b.revisionNumber - a.revisionNumber).map(rev => (
                  <div key={rev.id} className="text-xs border-b border-[var(--leon-line)] last:border-0 pb-1.5 last:pb-0">
                    <span className="font-semibold">Rev {rev.revisionNumber}</span>
                    <span className="text-[var(--leon-black)]/40"> · {fmtDate(rev.date)} · {rev.changedBy}</span>
                    <p className="text-[var(--leon-black)]/60 mt-0.5">{rev.change}</p>
                    {rev.meeting && rev.meeting.held && (
                      <div className="mt-1 rounded border border-[var(--leon-line)] bg-[var(--leon-cream)]/50 px-2 py-1.5">
                        <span className="font-semibold">🤝 Selection meeting</span>
                        <span className="text-[var(--leon-black)]/50"> · {fmtDate(rev.meeting.date)}</span>
                        {rev.meeting.location && <span className="text-[var(--leon-black)]/50"> · {rev.meeting.location}</span>}
                        <span className="ml-1.5">
                          <Badge tone={rev.meeting.clientPresent ? 'green' : 'yellow'}>
                            {rev.meeting.clientPresent ? 'Client present' : 'Client not present'}
                          </Badge>
                        </span>
                        {rev.meeting.attendees && (
                          <p className="text-[var(--leon-black)]/55 mt-0.5">Present: {rev.meeting.attendees}</p>
                        )}
                        {rev.meeting.notes && (
                          <p className="text-[var(--leon-black)]/55 mt-0.5 whitespace-pre-wrap">{rev.meeting.notes}</p>
                        )}
                      </div>
                    )}
                  </div>
                ))}
              </div>
            </Collapsible>
          </div>
        )}
      </div>
    </Collapsible>
    {/* Outside the Collapsible on purpose: its children unmount when the
        section is closed, and the Lock button sits in the header, which does
        not — so a modal filed as a child simply never appeared. */}
    {locking && (
      <LockSelectionsModal ctx={ctx} project={project} scope={scope} onClose={() => setLocking(false)} />
    )}
    </>
  );
}

function PrintHeader({ project, account, title }) {
  return (
    <div className="mb-6 flex items-center justify-between border-b-2 border-black pb-3">
      <div className="flex items-center gap-3">
        <img src="logo/leon-mark.svg" alt="LEON" className="h-20 w-auto" />
        <div>
          <div className="font-brand font-bold text-lg">LEON OPERATIONS HUB</div>
          <div className="text-xs text-gray-500">{COMPANY_PRINT_ADDRESS}</div>
          <div className="text-sm">{project.name} ({project.projectNumber})</div>
          <div className="text-xs text-gray-500">{account ? account.name : ''}</div>
        </div>
      </div>
      <div className="text-right">
        <div className="font-bold">{title}</div>
        <div className="text-xs text-gray-500">Printed {fmtDate(todayISO())}</div>
      </div>
    </div>
  );
}

// ---- Documents ---------------------------------------------------------
// ---- Drawing Sets (project-level, received during Lead so take-off can start) --
// Raising a request to have a take-off produced from a drawing set.
// IMPORTANT: this does not call an AI — the app has no backend and no model
// access. It records what was asked for and against which drawings, and the
// finished take-off is uploaded back onto the request in the Take-offs Hub.
// The record is shaped so a real integration later fills the same fields.
// ✨ AI Generate — raise a request for anything we'd normally draw up by hand,
// and hand the brief to Claude.
//
// The hand-off itself lives in lib.jsx as openInAi(), shared with every other
// AI control in the app, and which assistant to use is asked rather than
// assumed — see AiHandoffModal in components.jsx.

function RequestAiTakeoffModal({ open, drawingSet, onClose, ctx, project }) {
  const [deliverable, setDeliverable] = useState('Take-Off');
  const [scopeId, setScopeId] = useState('');
  const [instructions, setInstructions] = useState('');
  const [handoff, setHandoff] = useState(true);
  const [chooser, setChooser] = useState(false);
  useEffect(() => { if (open) { setDeliverable('Take-Off'); setScopeId(''); setInstructions(''); setHandoff(true); setChooser(false); } }, [open]);
  if (!drawingSet) return null;
  const scope = project.scopes.find(s => s.id === scopeId) || null;
  const chosen = AI_DELIVERABLES.find(d => d.key === deliverable) || AI_DELIVERABLES[0];
  const prompt = buildAiRequestPrompt({
    deliverable, project, scope, drawingSet, instructions, company: ctx.companyProfile,
  });
  function submit() {
    ctx.requestAiTakeoff(project.id, {
      deliverable,
      drawingSetId: drawingSet.id,
      drawingSetName: `${drawingSet.name || 'Drawing set'} — Rev ${drawingSet.revision}`,
      scopeId: scopeId || null,
      department: scope ? scopeDepartment(scope) : null,
      instructions,
    });
    // The request is recorded either way; the hand-off is a second step, and
    // which assistant gets it is the person's choice, not a default we picked.
    if (handoff) { setChooser(true); return; }
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title="✨ AI Generate"
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <div className="flex-1" />
               <Button onClick={submit}>{handoff ? 'Raise & choose an assistant' : 'Raise request'}</Button></>}>
      <div className="space-y-3">
        <div className="border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-[var(--leon-cream)]/60">
          <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">Drawing set</p>
          <p className="text-sm font-semibold">{drawingSet.name} &mdash; Rev {drawingSet.revision}</p>
          <p className="text-xs text-[var(--leon-black)]/50">Received {fmtDate(drawingSet.dateReceived)} from {drawingSet.source || '—'}</p>
        </div>

        <Field label="What do you need?">
          <div className="grid sm:grid-cols-2 gap-1.5">
            {AI_DELIVERABLES.map(d => (
              <button key={d.key} type="button" onClick={() => setDeliverable(d.key)}
                className={`text-left border rounded-lg px-3 py-2 ${deliverable === d.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown-light)]'}`}>
                <span className="text-sm font-semibold">{d.key}</span>
                <span className="block text-[11px] text-[var(--leon-black)]/50 leading-snug">{d.hint}</span>
              </button>
            ))}
          </div>
        </Field>

        <Field label="Which scope is it for?" hint="Files the result under that scope, and puts the scope's details in the brief.">
          <Select value={scopeId} onChange={e => setScopeId(e.target.value)}>
            <option value="">— the whole set —</option>
            {project.scopes.map(sc => <option key={sc.id} value={sc.id}>{sc.name}{sc.familyName ? ` — ${sc.familyName}` : ''}</option>)}
          </Select>
        </Field>

        <Field label="The brief" hint={`Be specific — this is what the ${deliverable.toLowerCase()} gets produced from.`}>
          <TextArea rows={4} value={instructions} onChange={e => setInstructions(e.target.value)}
            placeholder={'e.g. all base and upper cabinets on sheets A-201 to A-204, by unit type, with linear feet of countertop and the door/drawer counts per unit'} />
        </Field>

        <label className="flex items-start gap-2 text-sm cursor-pointer">
          <input type="checkbox" checked={handoff} onChange={e => setHandoff(e.target.checked)} className="mt-0.5 w-4 h-4 accent-[var(--leon-brown)]" />
          <span>
            <b>Open it in Claude when I submit</b>
            <span className="block text-[11px] text-[var(--leon-black)]/50 leading-snug">
              You choose Claude or ChatGPT next; the brief is copied to your clipboard and that
              assistant is opened with it already written in &mdash; the desktop app if it is installed on
              this machine, otherwise the website. <b>Attach the drawing set yourself in that window</b>
              &mdash; the Hub cannot send files to it.
            </span>
          </span>
        </label>

        <details className="border border-[var(--leon-line)] rounded-lg">
          <summary className="px-3 py-2 text-xs font-semibold text-[var(--leon-brown)] cursor-pointer">Preview the brief</summary>
          <pre className="px-3 pb-3 text-[11px] leading-relaxed whitespace-pre-wrap font-sans">{prompt}</pre>
        </details>

        <p className="text-[11px] text-[var(--leon-black)]/50 border-l-2 border-[var(--leon-yellow)] pl-2 py-1">
          <b>The Hub records the request; it does not generate anything itself.</b> The work is done in
          the assistant you pick and the result is uploaded back onto this request under Take-offs Hub
          &rarr; AI Requests. Nothing is sent anywhere automatically.
        </p>
      </div>
      <AiHandoffModal open={chooser} onClose={() => { setChooser(false); onClose(); }}
        title="✨ AI Generate — who should do this?" brief={prompt} />
    </Modal>
  );
}

function DrawingSetsTab({ ctx, project }) {
  const [aiFor, setAiFor] = useState(null);
  const editable = ctx.canEdit('drawingSets');
  const [showAdd, setShowAdd] = useState(false);
  const [voidFor, setVoidFor] = useState(null);
  const sorted = [...project.drawingSets].sort((a, b) => (a.dateReceived < b.dateReceived ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end gap-2 mb-2">
        <ShareButton ctx={ctx} projectId={project.id} subjectKey={`drawingSets:${project.id}`}
          subject={`Drawing Sets — ${project.name}`}
          summary={`${sorted.length} set${sorted.length === 1 ? '' : 's'}`}
          items={sorted.map(d => ({
            id: d.id, label: `${d.name || 'Drawing set'} — Rev ${d.revision}`,
            sub: `${d.status === 'Void' ? 'VOID · ' : ''}Received ${fmtDate(d.dateReceived)} from ${d.source || '—'}`,
          }))} />
        {editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Drawing Set</Button>}
      </div>
      <Collapsible id={`${project.id}-drawing-sets`} title="Drawing Sets" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No drawing sets received yet." /> : (
          <div className="space-y-1.5">
            {sorted.map(d => (
              <div key={d.id} className={`flex items-center justify-between gap-2 border rounded-lg px-3 py-2 ${d.status === 'Void' ? 'border-[var(--leon-line)] opacity-60' : 'border-[var(--leon-line)]'}`}>
                <div className="min-w-0">
                  <p className="text-sm font-semibold truncate flex items-center gap-1.5">
                    <FileField name={d.name} url={d.fileUrl} onChange={(fname, url) => ctx.updateDrawingSet(project.id, d.id, { name: fname, fileUrl: url })} editable={editable && d.status !== 'Void'} />
                    <span className="text-[var(--leon-black)]/40 font-normal">Rev {d.revision}</span>
                    {d.status === 'Void' && <Badge tone="red">Void</Badge>}
                  </p>
                  <p className="text-xs text-[var(--leon-black)]/50">Received {fmtDate(d.dateReceived)} from {d.source}{d.sharedBy ? ` (${d.sharedBy})` : ''}{d.note ? ` — ${d.note}` : ''}</p>
                  <p className="text-xs text-[var(--leon-black)]/50">Assigned to review: <strong className="text-[var(--leon-black)]">{personName(ctx.teamDirectory, d.reviewerAssigneeId)}</strong></p>
                  {d.status === 'Void' && <p className="text-xs text-[var(--leon-red)]">Voided {fmtDate(d.voidedDate)} by {d.voidedBy}: {d.voidReason}</p>}
                </div>
                <div className="flex items-center gap-2 shrink-0">
                  {/* Admin-only for now. Raises a request against this drawing
                      set; the result is uploaded back onto it in the Take-offs
                      Hub. Nothing here calls a model — see makeAiTakeoffRequest. */}
                  {ctx.currentRole === 'Admin' && d.status !== 'Void' && (
                    <Button size="sm" variant="outline" onClick={() => setAiFor(d)}>&#10024; AI Generate</Button>
                  )}
                  {editable && d.status !== 'Void' && <button onClick={() => setVoidFor(d)} className="text-xs text-[var(--leon-red)] font-semibold whitespace-nowrap">Void</button>}
                </div>
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <AddDrawingSetModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <RequestAiTakeoffModal open={!!aiFor} drawingSet={aiFor} onClose={() => setAiFor(null)} ctx={ctx} project={project} />
      <VoidDrawingSetModal open={!!voidFor} drawingSet={voidFor} onClose={() => setVoidFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function VoidDrawingSetModal({ open, drawingSet, onClose, ctx, project }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  if (!drawingSet) return null;
  function submit() { if (!reason.trim()) return; ctx.voidDrawingSet(project.id, drawingSet.id, reason.trim()); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Void Drawing Set — ${drawingSet.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit} disabled={!reason.trim()}>Void Drawing Set</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Drawing Sets can't be deleted — voiding keeps the record on file, marked Void, with the reason below.</p>
        <Field label="Reason for Voiding" hint="Required"><TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} autoFocus /></Field>
      </div>
    </Modal>
  );
}
function AddDrawingSetModal({ open, onClose, ctx, project }) {
  const blank = { name: '', fileUrl: null, revision: 'A', dateReceived: todayISO(), source: '', sharedBy: '', note: '', reviewerAssigneeId: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() { if (!form.name.trim() || !form.reviewerAssigneeId) return; ctx.addDrawingSet(project.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Drawing Set" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!form.name.trim() || !form.reviewerAssigneeId}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Attachment"><FileField name={form.name} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, name: fname, fileUrl: url })} editable /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Revision"><TextInput value={form.revision} onChange={e => setForm({ ...form, revision: e.target.value })} /></Field>
          <Field label="Date Received"><TextInput type="date" value={form.dateReceived} onChange={e => setForm({ ...form, dateReceived: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Source" hint="Who shared it (GC, Architect, Developer…)"><TextInput value={form.source} onChange={e => setForm({ ...form, source: e.target.value })} /></Field>
          <Field label="Shared By (person)"><TextInput value={form.sharedBy} onChange={e => setForm({ ...form, sharedBy: e.target.value })} /></Field>
        </div>
        <Field label="Assigned to Review" hint="Required — a Drawing Set must always have a reviewer.">
          <Select value={form.reviewerAssigneeId} onChange={e => setForm({ ...form, reviewerAssigneeId: e.target.value })}>
            <option value="">— select reviewer —</option>
            {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Take-Offs (project-level, prepared from the drawing sets above) -----------
// AI take-offs requested from a drawing set. Each row is a request; the
// finished take-off is uploaded onto it here, which marks it Delivered.
function AiTakeoffsBlock({ ctx, project }) {
  const reqs = [...(project.aiTakeoffRequests || [])].sort((a, b) => (a.requestedDate < b.requestedDate ? 1 : -1));
  return (
    <Collapsible id={`${project.id}-ai-takeoffs`} title="AI Requests" count={reqs.length}
      right={<Badge tone="neutral">Admin only</Badge>}>
      <p className="text-xs text-[var(--leon-black)]/50 mb-2">
        Raised from a drawing set with the <b>&#10024; AI Generate</b> button &mdash; take-offs, shop
        drawings, submittals, renders. The Hub records the request and hands the brief to Claude; the
        work is done there and the result is uploaded back here, which is exactly the record a real
        integration would fill in on its own.
      </p>
      {reqs.length === 0 ? <EmptyState text="Nothing requested yet." /> : (
        <div className="space-y-1.5">
          {reqs.map(r => (
            <div key={r.id} className="border border-[var(--leon-line)] rounded-lg p-3">
              <div className="flex items-start gap-2 flex-wrap">
                <div className="min-w-0 flex-1">
                  <p className="text-sm font-bold">
                    {r.deliverable ? <Badge tone="brown">{r.deliverable}</Badge> : null}{' '}
                    {r.drawingSetName || 'Drawing set'}
                  </p>
                  <p className="text-xs text-[var(--leon-black)]/50">
                    {[r.scopeId ? (project.scopes.find(s => s.id === r.scopeId) || {}).name : 'Whole set',
                      `Requested ${fmtDate(r.requestedDate)} by ${r.requestedBy}`].filter(Boolean).join(' · ')}
                  </p>
                </div>
                <Select value={r.status} onChange={e => ctx.updateAiTakeoff(project.id, r.id, { status: e.target.value })} className="!w-36 !py-1 !text-xs">
                  {AI_TAKEOFF_STATUSES.map(st => <option key={st}>{st}</option>)}
                </Select>
                <IconBtn title="Remove this request" onClick={() => { if (confirm('Remove this AI take-off request?')) ctx.removeAiTakeoff(project.id, r.id); }}>&#10005;</IconBtn>
              </div>
              {r.instructions && (
                <p className="text-xs text-[var(--leon-black)]/60 mt-1.5 bg-[var(--leon-cream)]/60 rounded px-2 py-1.5 whitespace-pre-wrap">{r.instructions}</p>
              )}
              <div className="flex items-center gap-3 mt-2 flex-wrap text-xs">
                <span className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">Result</span>
                <FileField name={r.resultFile} url={r.resultFileUrl} editable projectId={project.id} label={r.drawingSetName}
                  onChange={(fname, url) => ctx.updateAiTakeoff(project.id, r.id, { resultFile: fname, resultFileUrl: url })}
                  placeholder="Nothing uploaded yet" />
                {r.deliveredDate && <span className="text-[var(--leon-black)]/45">Delivered {fmtDate(r.deliveredDate)} by {r.deliveredBy}</span>}
              </div>
            </div>
          ))}
        </div>
      )}
    </Collapsible>
  );
}

function TakeOffsTab({ ctx, project }) {
  const editable = ctx.canEdit('takeOffs');
  const [showAdd, setShowAdd] = useState(false);
  const [revisionOf, setRevisionOf] = useState(null);
  const [deptFilter, setDeptFilter] = useState('All');
  const [openAtt, setOpenAtt] = useState(null);   // which take-off has its filing open
  // The header department scope applies first (a Windows-only user never sees
  // interiors take-offs); the chips below are a further narrowing on top.
  const inScope = project.takeOffs.filter(t => ctx.activeDepartment === ALL_DEPARTMENTS || !t.department || t.department === ctx.activeDepartment);
  const sorted = inScope
    .filter(t => deptFilter === 'All' || (deptFilter === 'Unassigned' ? !t.department : t.department === deptFilter))
    .sort((a, b) => (a.date < b.date ? 1 : -1));
  const countFor = d => inScope.filter(t => d === 'Unassigned' ? !t.department : t.department === d).length;
  return (
    <div>
      <div className="flex items-center justify-between gap-2 mb-2 flex-wrap">
        <div className="flex gap-1 flex-wrap items-center">
          {ctx.currentRole === 'Admin' && (project.aiTakeoffRequests || []).length > 0 && (
            <Badge tone="yellow">{(project.aiTakeoffRequests || []).filter(r => r.status !== 'Delivered').length} AI pending</Badge>
          )}
          <ShareButton ctx={ctx} projectId={project.id} subjectKey={`takeOffs:${project.id}`}
            subject={`Take-offs — ${project.name}`}
            summary={`${(project.takeOffs || []).length} take-off${(project.takeOffs || []).length === 1 ? '' : 's'}`}
            items={(project.takeOffs || []).map(t => ({
              id: t.id, label: t.name || t.title || 'Take-off',
              sub: `${(t.items || t.lines || []).length} line${(t.items || t.lines || []).length === 1 ? '' : 's'}${t.department ? ` · ${t.department}` : ''}`,
            }))} />
          {['All', ...DEPARTMENTS, 'Unassigned'].map(d => (
            <button key={d} onClick={() => setDeptFilter(d)}
              className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition ${deptFilter === d ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60'}`}>
              {d} <span className="opacity-50">{d === 'All' ? inScope.length : countFor(d)}</span>
            </button>
          ))}
        </div>
        {/* One group, so the row's justify-between does not push these two to
            opposite ends of the toolbar. They are the two ways to start a
            take-off and belong beside each other. */}
        {editable && (
          <div className="flex items-center gap-2 shrink-0">
            <StartFromTakeoffTemplateButton ctx={ctx} project={project} />
            <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Take-Off</Button>
          </div>
        )}
      </div>
      {/* Admin-only until the flow is proven. Sits above the take-offs proper
          because a pending request is a thing waiting on someone. */}
      {ctx.currentRole === 'Admin' && <AiTakeoffsBlock ctx={ctx} project={project} />}
      <Collapsible id={`${project.id}-take-offs`} title="Take-Offs" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No take-offs yet." /> : (
          <div className="space-y-1.5">
            {sorted.map(t => {
              const scope = project.scopes.find(s => s.id === t.scopeId);
              const attCount = (t.attachments || []).length;
              return (
                <div key={t.id} className="border border-[var(--leon-line)] rounded-lg">
                <div className="flex items-center justify-between gap-2 px-3 py-2">
                  <div className="min-w-0">
                    <p className="text-sm font-semibold truncate flex items-center gap-1.5">
                      <FileField name={t.name} url={t.fileUrl} onChange={(fname, url) => ctx.updateTakeOff(project.id, t.id, { name: fname, fileUrl: url })} editable={editable} />
                      <span className="text-[var(--leon-black)]/40 font-normal">Rev {t.revision}</span>
                      {t.department
                        ? <Badge tone="neutral">{t.department === 'Windows' ? '🪟' : '🏠'} {t.department}</Badge>
                        : <Badge tone="yellow">Unassigned</Badge>}
                    </p>
                    <p className="text-xs text-[var(--leon-black)]/50">{fmtDate(t.date)} · prepared by {t.preparedBy}{scope ? ` · ${scope.name}` : ''}{t.note ? ` — ${t.note}` : ''}</p>
                  </div>
                  {editable && (
                    <div className="flex items-center gap-2 shrink-0">
                      <Select value={t.department || ''} onChange={e => ctx.updateTakeOff(project.id, t.id, { department: e.target.value || null })} className="!w-28 !py-1 !text-xs">
                        <option value="">Unassigned</option>
                        {DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
                      </Select>
                      <button onClick={() => setRevisionOf(t)} className="text-xs text-[var(--leon-brown)] font-semibold whitespace-nowrap">+ Add Revision</button>
                      <IconBtn title="Remove" onClick={() => ctx.removeTakeOff(project.id, t.id)}>✕</IconBtn>
                    </div>
                  )}
                  {/* The count is on the row so you can see at a glance which
                      revision was actually filed against, without opening each. */}
                  <button onClick={() => setOpenAtt(openAtt === t.id ? null : t.id)}
                    title="What this take-off was measured from"
                    className={`shrink-0 px-2 py-1 rounded-md text-xs font-semibold border transition ${openAtt === t.id ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60'}`}>
                    🗂️ {attCount || 'Add'}
                  </button>
                </div>
                {openAtt === t.id && (
                  <TakeOffAttachments ctx={ctx} project={project} takeOff={t} editable={editable} />
                )}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddTakeOffModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <AddTakeOffModal open={!!revisionOf} onClose={() => setRevisionOf(null)} ctx={ctx} project={project} baseTakeOff={revisionOf} />
    </div>
  );
}
// "Start from template" — LEON's own take-off workbook as a real Leon Sheets
// document, filed against this job and opened straight away. It calls the SAME
// builder Sheets' own Start-from list uses, so there is one template rather
// than two that drift apart. Guarded on the builder existing, so a missing
// script leaves the button off rather than breaking the hub.
function StartFromTakeoffTemplateButton({ ctx, project }) {
  if (typeof leonTakeoffTemplateBody !== 'function') return null;
  function start() {
    const doc = makeOfficeDocument({
      name: 'Take-Off — ' + project.name,
      app: 'sheet', folder: 'Project', projectId: project.id,
      body: leonTakeoffTemplateBody(),
    }, ctx.currentUserName);
    doc.activity = [{ id: uid('act'), date: todayISO(), by: ctx.currentUserName,
      text: 'Started from the LEON Take-Off Template.' }];
    ctx.setOfficeDocs(prev => [doc].concat(prev || []));
    // The job records that a take-off was started, so the workbook can be
    // traced back here later rather than floating in the document library.
    ctx.updateProject(project.id, draft => {
      ctx.logAction(draft, 'Started a take-off from the LEON Take-Off Template — "' + doc.name + '".');
    });
    ctx.goSoftware('office:sheet', { doc: doc.id });
  }
  return (
    /* Solid BLACK, not the brown primary: this sits beside "+ Add Take-Off",
       and two brown buttons would read as one action offered twice. Black is
       already the app's strong-action colour (the masthead, the active nav),
       so it stands out without inventing a new one. It was `ghost`, which is
       the quietest variant there is — invisible next to a solid button. */
    <Button size="sm" variant="black" onClick={start}
      title="Create this job's take-off workbook in LEON Sheets — a tab per scope, with the waste allowances and the unit matrix already set up">
      📐 Start from LEON template
    </Button>
  );
}
// What a take-off was measured FROM, filed under the kinds LEON actually uses:
// the finish schedule, the client's door schedule, door elevations, kitchen and
// bathroom layouts. Every kind is shown whether or not it holds anything, so
// the panel says what is MISSING as much as what is there.
//
// Pictures follow the quotation deck's rule exactly: add as many as you like,
// remove any, and the caption is optional — left blank it shows nothing at all
// rather than an empty line.
function TakeOffAttachments({ ctx, project, takeOff, editable }) {
  const atts = takeOff.attachments || [];
  const [busy, setBusy] = useState('');
  const [err, setErr] = useState('');
  const [kind, setKind] = useState(null);
  const fileRef = useRef(null);

  const weight = atts.reduce((a, x) => a + (x.bytes || 0), 0);

  function pick(k) {
    setKind(k); setErr('');
    if (fileRef.current) { fileRef.current.value = ''; fileRef.current.click(); }
  }
  function read(file) {
    return new Promise((res, rej) => {
      const r = new FileReader();
      r.onload = () => res(r.result);
      r.onerror = () => rej(new Error('read'));
      r.readAsDataURL(file);
    });
  }
  async function onFiles(e) {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    const problems = [];
    for (let i = 0; i < files.length; i++) {
      const f = files[i];
      setBusy('Adding ' + (i + 1) + ' of ' + files.length + '…');
      try {
        const raw = await read(f);
        if (/^image\//.test(f.type)) {
          // Downscaled before it is ever stored. shrinkSignature is a general
          // image shrinker despite its name — it caps the width and keeps
          // whichever of PNG/JPEG comes out smaller.
          const s = await shrinkSignature(raw, TAKEOFF_PICTURE_MAX_PX);
          ctx.addTakeOffAttachment(project.id, takeOff.id, {
            kind: kind, name: f.name, url: s.url, picture: true,
            w: s.w, h: s.h, bytes: s.bytes,
          });
        } else if (f.size > TAKEOFF_FILE_LIMIT_BYTES) {
          problems.push(f.name + ' is ' + fmtBytes(f.size));
        } else {
          ctx.addTakeOffAttachment(project.id, takeOff.id, {
            kind: kind, name: f.name, url: raw, picture: false, bytes: f.size,
          });
        }
      } catch (x) { problems.push(f.name + ' could not be read'); }
    }
    setBusy('');
    setErr(problems.length
      ? problems.join('; ') + '. Documents are capped at ' + fmtBytes(TAKEOFF_FILE_LIMIT_BYTES)
        + ' because every file here is stored in this browser. Pictures are downscaled and always fit.'
      : '');
  }

  return (
    <div className="border-t border-[var(--leon-line)] px-3 py-3 bg-[var(--leon-cream)]/40">
      <div className="flex items-center justify-between gap-2 mb-2">
        <p className="text-xs font-semibold text-[var(--leon-black)]/70">
          What this take-off was measured from
        </p>
        <span className="text-[11px] text-[var(--leon-black)]/45">
          {atts.length} filed{weight ? ' · ' + fmtBytes(weight) : ''}
        </span>
      </div>
      {busy && <p className="text-[11px] text-[var(--leon-brown)] mb-2">{busy}</p>}
      {err && <p className="text-[11px] text-[var(--leon-red)] mb-2">{err}</p>}
      <input ref={fileRef} type="file" multiple className="hidden"
        accept="image/*,.pdf,.dwg,.dxf,.xlsx,.xls,.csv,.doc,.docx"
        onChange={onFiles} />
      <div className="space-y-2">
        {TAKEOFF_DOC_KINDS.map(k => {
          const mine = atts.filter(a => a.kind === k.key);
          const pics = mine.filter(a => a.picture);
          const docs = mine.filter(a => !a.picture);
          return (
            <div key={k.key} className="border border-[var(--leon-line)] rounded-lg bg-white px-3 py-2">
              <div className="flex items-center justify-between gap-2">
                <p className="text-xs font-semibold">
                  <span aria-hidden="true" className="mr-1">{k.icon}</span>{k.label}
                  {mine.length > 0 && <span className="text-[var(--leon-black)]/40 font-normal"> · {mine.length}</span>}
                </p>
                {editable && (
                  <button onClick={() => pick(k.key)}
                    className="text-[11px] font-semibold text-[var(--leon-brown)] whitespace-nowrap">
                    + Add {mine.length ? 'more' : 'picture or document'}
                  </button>
                )}
              </div>
              {docs.length > 0 && (
                <div className="mt-1.5 space-y-1">
                  {docs.map(a => (
                    <div key={a.id} className="flex items-center gap-2 text-xs">
                      <AttachmentLink name={a.name} url={a.url} />
                      <span className="text-[var(--leon-black)]/35">{fmtBytes(a.bytes)}</span>
                      {editable && (
                        <IconBtn title="Remove" onClick={() => ctx.removeTakeOffAttachment(project.id, takeOff.id, a.id)}>✕</IconBtn>
                      )}
                    </div>
                  ))}
                </div>
              )}
              {pics.length > 0 && (
                <div className="mt-2 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
                  {pics.map(a => (
                    <div key={a.id} className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
                      <Photo src={a.url} alt={a.caption || a.name} className="w-full h-24 object-cover" />
                      <div className="px-1.5 py-1">
                        {editable ? (
                          <TextInput value={a.caption || ''} placeholder="Caption (optional)"
                            className="!text-[11px] !py-0.5"
                            onChange={e => ctx.updateTakeOffAttachment(project.id, takeOff.id, a.id, { caption: e.target.value })} />
                        ) : (
                          a.caption ? <p className="text-[11px] leading-snug">{a.caption}</p> : null
                        )}
                        <div className="flex items-center justify-between mt-0.5">
                          <span className="text-[10px] text-[var(--leon-black)]/35 truncate">{fmtBytes(a.bytes)}</span>
                          {editable && (
                            <IconBtn title="Remove picture" onClick={() => ctx.removeTakeOffAttachment(project.id, takeOff.id, a.id)}>✕</IconBtn>
                          )}
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          );
        })}
      </div>
      <p className="text-[10px] text-[var(--leon-black)]/40 mt-2">
        Pictures are downscaled to {TAKEOFF_PICTURE_MAX_PX}px on the way in and stored in this
        browser, so keep the set to what a reviewer actually needs. A caption left blank shows nothing.
      </p>
    </div>
  );
}
function AddTakeOffModal({ open, onClose, ctx, project, baseTakeOff }) {
  const blank = { name: '', fileUrl: null, revision: 1, date: todayISO(), preparedBy: ctx.currentUserName, scopeId: '', note: '', department: ctx.activeDepartment === ALL_DEPARTMENTS ? '' : ctx.activeDepartment };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (baseTakeOff) {
      const siblingRevisions = project.takeOffs.filter(t => t.name === baseTakeOff.name && t.scopeId === baseTakeOff.scopeId).map(t => t.revision);
      setForm({ ...blank, name: baseTakeOff.name, scopeId: baseTakeOff.scopeId || '', revision: Math.max(...siblingRevisions) + 1 });
    } else {
      setForm(blank);
    }
  }, [open, baseTakeOff]);
  function submit() { if (!form.name.trim()) return; ctx.addTakeOff(project.id, { ...form, revision: Number(form.revision), scopeId: form.scopeId || null, department: form.department || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Take-Off" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Attachment"><FileField name={form.name} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, name: fname, fileUrl: url })} editable /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Revision"><TextInput type="number" min="1" value={form.revision} onChange={e => setForm({ ...form, revision: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Prepared By"><TextInput value={form.preparedBy} onChange={e => setForm({ ...form, preparedBy: e.target.value })} /></Field>
        {/* Picking a scope also sets the department, since the scope already
            knows which one it belongs to — the department stays editable for
            take-offs done before any scope exists. */}
        <Field label="Related Scope (optional)"><Select value={form.scopeId} onChange={e => { const sc = project.scopes.find(x => x.id === e.target.value); setForm({ ...form, scopeId: e.target.value, department: sc ? scopeDepartment(sc, ctx.scopeLibrary) : form.department }); }}><option value="">— none yet —</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <Field label="Department" hint="Which department this take-off is for — used to filter the Take-offs list.">
          <Select value={form.department || ''} onChange={e => setForm({ ...form, department: e.target.value })}>
            <option value="">Unassigned</option>
            {DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
          </Select>
        </Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Renders (organized by named set, each with its own revision history;
// a set's scope is optional — not every render is tied to one) -------------
function RendersTab({ ctx, project }) {
  const editable = ctx.canEdit('renders');
  const [showAdd, setShowAdd] = useState(false);
  const [revisionOf, setRevisionOf] = useState(null);
  const sets = [...project.renderSets].sort((a, b) => (a.createdDate < b.createdDate ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end gap-2 mb-2">
        <ShareButton ctx={ctx} projectId={project.id} subjectKey={`renders:${project.id}`}
          subject={`Renders — ${project.name}`}
          summary={`${(project.renderSets || []).length} render set${(project.renderSets || []).length === 1 ? '' : 's'}`}
          items={(project.renderSets || []).map(r => ({
            id: r.id, label: r.name || 'Render set',
            sub: `${(r.images || r.files || []).length} image${(r.images || r.files || []).length === 1 ? '' : 's'}`,
          }))} />
        {editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Render Set</Button>}
      </div>
      {sets.length === 0 ? <EmptyState text="No render sets yet." /> : sets.map(set => {
        const scope = set.scopeId ? project.scopes.find(s => s.id === set.scopeId) : null;
        const sortedRevisions = [...set.revisions].sort((a, b) => b.revisionNumber - a.revisionNumber);
        return (
          <Collapsible key={set.id} title={set.name} count={set.revisions.length} right={<Badge tone="neutral">{scope ? scope.name : 'No scope'}</Badge>}>
            {editable && (
              <div className="flex justify-end gap-3 mb-2">
                <button onClick={() => setRevisionOf(set)} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Revision</button>
                <button onClick={() => ctx.removeRenderSet(project.id, set.id)} className="text-xs text-[var(--leon-red)] font-semibold">Remove Set</button>
              </div>
            )}
            <div className="space-y-3">
              {sortedRevisions.map(rev => (
                <div key={rev.id} className="border-t border-[var(--leon-line)] first:border-t-0 pt-2 first:pt-0">
                  <div className="flex items-center gap-1.5 mb-1.5">
                    <Badge tone={rev.revisionNumber === sortedRevisions[0].revisionNumber ? 'green' : 'neutral'}>Rev {rev.revisionNumber}</Badge>
                    <span className="text-xs text-[var(--leon-black)]/50">{fmtDate(rev.date)}{rev.sharedWithClient ? ' · Shared with client' : ''}</span>
                  </div>
                  {rev.note && <p className="text-xs text-[var(--leon-black)]/50 mb-1.5">{rev.note}</p>}
                  <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-2">
                    {rev.images.length === 0 ? <EmptyState text="No images." /> : rev.images.map(img => (
                      <div key={img.id} className="aspect-[4/3] bg-[var(--leon-cream)] rounded-lg overflow-hidden flex items-center justify-center">
                        {img.imageUrl ? <Photo src={img.imageUrl} alt={img.caption || set.name} title={set.name} caption={img.caption} className="w-full h-full object-cover" /> : <span className="text-[var(--leon-black)]/25 text-3xl">🖼</span>}
                      </div>
                    ))}
                  </div>
                </div>
              ))}
            </div>
          </Collapsible>
        );
      })}
      <AddRenderModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <AddRenderModal open={!!revisionOf} onClose={() => setRevisionOf(null)} ctx={ctx} project={project} baseSet={revisionOf} />
    </div>
  );
}
function AddRenderModal({ open, onClose, ctx, project, baseSet }) {
  const blank = { scopeId: '', name: '', images: [{ id: uid('rimg'), imageUrl: null, caption: '' }], date: todayISO(), note: '', sharedWithClient: false };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(baseSet ? { ...blank, scopeId: baseSet.scopeId || '', name: baseSet.name } : blank); }, [open, baseSet]);
  function submit() {
    if (!form.name.trim()) return;
    const payload = { ...form, scopeId: form.scopeId || null, images: form.images.filter(i => i.imageUrl) };
    if (baseSet) ctx.addRenderSetRevision(project.id, baseSet.id, payload);
    else ctx.addRenderSet(project.id, payload);
    onClose();
  }
  function setImage(id, url) { setForm(f => ({ ...f, images: f.images.map(i => i.id === id ? { ...i, imageUrl: url } : i) })); }
  function addImageSlot() { setForm(f => ({ ...f, images: [...f.images, { id: uid('rimg'), imageUrl: null, caption: '' }] })); }
  function removeImageSlot(id) { setForm(f => ({ ...f, images: f.images.filter(i => i.id !== id) })); }
  return (
    <Modal open={open} onClose={onClose} title={baseSet ? `Add Revision — ${baseSet.name}` : 'Add Render Set'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{baseSet ? 'Add Revision' : 'Add Render Set'}</Button></>}>
      <div className="space-y-3">
        <Field label="Set Name"><TextInput disabled={!!baseSet} value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. Kitchen Cabinetry — Concept Renders" /></Field>
        <Field label="Scope (optional)" hint="Renders don't need to be linked to a scope.">
          <Select disabled={!!baseSet} value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
            <option value="">— no scope —</option>
            {project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </Select>
        </Field>
        <Field label="Images">
          <div className="grid sm:grid-cols-2 gap-3">
            {form.images.map(img => (
              <div key={img.id} className="flex items-center gap-2">
                <ImagePicker url={img.imageUrl} onChange={url => setImage(img.id, url)} size={64} />
                {form.images.length > 1 && <IconBtn title="Remove image" onClick={() => removeImageSlot(img.id)}>✕</IconBtn>}
              </div>
            ))}
          </div>
          <button onClick={addImageSlot} className="text-xs text-[var(--leon-brown)] font-semibold mt-2">+ Add another image</button>
        </Field>
        <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
        <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={form.sharedWithClient} onChange={e => setForm({ ...form, sharedWithClient: e.target.checked })} /> Shared with client</label>
      </div>
    </Modal>
  );
}

const SCOPE_DOC_TYPES = ['Shop Drawings', 'Submittal'];
function DocumentsTab({ ctx, project }) {
  const editable = ctx.canEdit('documents');
  const [addFor, setAddFor] = useState(null);
  const [docDetailFor, setDocDetailFor] = useState(null);
  // The scope's own documents, plus any extra sections the team has added.
  // Shop Drawing / Submittal and Client Response are STRUCTURAL — they are part
  // of the approval workflow, not filing, so they are rendered separately below
  // and have no delete. Only sections created here can be removed.
  function DocRows({ scope, docs }) {
    if (!docs.length) return <EmptyState text="No documents." />;
    return (
      <table className="w-full text-xs">
        <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">File</th><th className="py-1">Type</th><th className="py-1">Rev</th><th className="py-1">Date</th><th className="py-1">Shared w/ Client</th><th className="py-1"></th></tr></thead>
        <tbody>
          {docs.map(d => (
            <tr key={d.id} className="border-t border-[var(--leon-line)]">
              <td className="py-1.5 font-semibold">
                <button type="button" onClick={() => setDocDetailFor({ ...d, scopeName: scope.name })} className="no-print mr-1.5 text-[var(--leon-black)]/40 hover:text-[var(--leon-brown)]" title="View details">&#9432;</button>
                <FileField name={d.name} url={d.fileUrl} projectId={project.id} label={scope.name}
                  onChange={(fname, url) => ctx.updateScopeDocument(project.id, scope.id, d.id, { name: fname, fileUrl: url })} editable={editable} />
              </td>
              <td className="py-1.5">{d.type}</td>
              <td className="py-1.5">R{d.revision}</td>
              <td className="py-1.5">{fmtDate(d.dateCreated)}</td>
              <td className="py-1.5">{d.sharedWithClient ? <Badge tone="green">Shared</Badge> : <Badge tone="neutral">Internal</Badge>}</td>
              <td className="py-1.5 text-right">
                {editable && (
                  <IconBtn title="Remove this document" onClick={() => {
                    if (confirm(`Remove "${d.name}" from ${scope.name}?`)) ctx.removeScopeDocument(project.id, scope.id, d.id);
                  }}>&#10005;</IconBtn>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }

  function ScopeDocsBlock({ scope }) {
    const [addingSection, setAddingSection] = useState(false);
    const [newSection, setNewSection] = useState('');
    const [renaming, setRenaming] = useState(null);
    const [renameTo, setRenameTo] = useState('');
    const sections = scope.documentSections || [];
    const general = (scope.documents || []).filter(d => !d.sectionId || !sections.some(x => x.id === d.sectionId));
    return (
      <div className="mb-5">
        <Collapsible title={scope.name} count={scope.documents.length} right={
          <span className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
            {/* This scope's drawings, grouped by submittal thread — tick a
                whole thread or pick individual revisions. */}
            <ShareButton ctx={ctx} projectId={project.id} subjectKey={`scopeDrawings:${scope.id}`}
              subject={`Shop Drawings — ${scope.name}`}
              summary={project.name}
              items={scopeDrawingPackageItems(scope)} />
            {editable && <Button size="sm" variant="ghost" onClick={() => setAddFor({ scope })}>+ Add Document</Button>}
          </span>
        }>
          <DocRows scope={scope} docs={general} />
        </Collapsible>

        <SubmittalsBlock ctx={ctx} project={project} scope={scope} editable={editable} />
        {scope.familyName === 'Casework' && <ApplianceSpecsBlock ctx={ctx} project={project} scope={scope} editable={editable} />}
        {(scope.familyName === 'Casework' || scope.familyName === 'Countertop') && <FixtureSpecsBlock ctx={ctx} project={project} scope={scope} editable={editable} />}
        {/* Sections the team added. Each holds its own documents and can be
            renamed or removed; removing one keeps its files, moving them back
            to the scope's general list. */}
        {sections.map(sec => {
          const docs = (scope.documents || []).filter(d => d.sectionId === sec.id);
          return (
            <div key={sec.id} className="pl-3 border-l-2 border-[var(--leon-line)] ml-2 mt-1">
            <Collapsible id={`docsec-${sec.id}`} title={sec.name} count={docs.length} right={
              <span className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
                {editable && <Button size="sm" variant="ghost" onClick={() => setAddFor({ scope, sectionId: sec.id, sectionName: sec.name })}>+ Add Document</Button>}
                {editable && <Button size="sm" variant="ghost" onClick={() => { setRenaming(sec.id); setRenameTo(sec.name); }}>Rename</Button>}
                {editable && (
                  <IconBtn title="Remove this section" onClick={() => {
                    if (confirm(`Remove the "${sec.name}" section? Its ${docs.length} document${docs.length === 1 ? '' : 's'} will move back to ${scope.name}'s documents — nothing is deleted.`))
                      ctx.removeScopeDocumentSection(project.id, scope.id, sec.id);
                  }}>&#10005;</IconBtn>
                )}
              </span>
            }>
              {renaming === sec.id && (
                <div className="flex items-center gap-1.5 mb-2">
                  <TextInput autoFocus value={renameTo} onChange={e => setRenameTo(e.target.value)} className="!w-64 !py-1 !text-xs" />
                  <Button size="sm" onClick={() => { if (renameTo.trim()) ctx.renameScopeDocumentSection(project.id, scope.id, sec.id, renameTo.trim()); setRenaming(null); }}>Save</Button>
                  <Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button>
                </div>
              )}
              <DocRows scope={scope} docs={docs} />
            </Collapsible>
            </div>
          );
        })}

        {editable && (
          <div className="pl-3 border-l-2 border-transparent ml-2 mt-1 mb-3">
            {addingSection ? (
              <div className="flex items-center gap-1.5">
                <TextInput autoFocus value={newSection} onChange={e => setNewSection(e.target.value)}
                  placeholder="e.g. Field Measurements, Certificates, Warranties" className="!w-72 !py-1 !text-xs" />
                <Button size="sm" onClick={() => { if (newSection.trim()) { ctx.addScopeDocumentSection(project.id, scope.id, newSection.trim()); setNewSection(''); setAddingSection(false); } }}>Add</Button>
                <Button size="sm" variant="ghost" onClick={() => setAddingSection(false)}>&#10005;</Button>
              </div>
            ) : (
              <button onClick={() => setAddingSection(true)} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Document Section</button>
            )}
          </div>
        )}

      </div>
    );
  }
  const overview = (
    <div>
      {project.scopes.length === 0 ? <EmptyState text="No scopes yet." /> : project.scopes.map(scope => <ScopeDocsBlock key={scope.id} scope={scope} />)}
    </div>
  );
  return (
    <div>
      <ScopeHubTabs ctx={ctx} project={project} overview={overview} renderScope={scope => <ScopeDocsBlock scope={scope} />} />
      <AddDocumentModal open={!!addFor} target={addFor} onClose={() => setAddFor(null)} ctx={ctx} project={project} />
      <RecordDetailModal open={!!docDetailFor} onClose={() => setDocDetailFor(null)} title={`Document — ${docDetailFor?.name || docDetailFor?.type || ''}`} printable
        fields={docDetailFor ? [
          { label: 'Scope', value: docDetailFor.scopeName }, { label: 'Type', value: docDetailFor.type }, { label: 'Revision', value: `R${docDetailFor.revision}` },
          { label: 'Date', value: fmtDate(docDetailFor.dateCreated) }, { label: 'Shared with Client', value: docDetailFor.sharedWithClient ? 'Yes' : 'No' },
        ] : []}
        attachments={docDetailFor && docDetailFor.fileUrl ? [{ name: docDetailFor.name, url: docDetailFor.fileUrl }] : []}
      />
    </div>
  );
}

// ---- Shop Drawing/Submittal vs Client Response classification (§1) --------
function SubmittalThreadCard({ ctx, project, scope, thread, isResponse, editable, submittalName }) {
  const [revModal, setRevModal] = useState(false);
  const [detailOpen, setDetailOpen] = useState(false);
  const sorted = [...thread.revisions].sort((a, b) => b.revisionNumber - a.revisionNumber);
  const linkedAppliances = (project.applianceInstances || []).filter(i => i.shopDrawingId === thread.id);
  const linkedFixtures = (project.fixtureInstances || []).filter(i => i.shopDrawingId === thread.id);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-2">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="cursor-pointer" onClick={() => setDetailOpen(true)}>
          <p className="text-sm font-bold hover:underline">{thread.name}</p>
          <p className="text-xs text-[var(--leon-black)]/50">
            {thread.vendorName && <>{thread.vendorName} · </>}
            {isResponse && submittalName && <>Responding to <strong>{submittalName}</strong> Rev. {thread.respondingToRevisionNumber} · </>}
            Created {fmtDate(thread.createdDate)} by {thread.createdBy}
          </p>
        </div>
        <div className="flex items-center gap-2">
          <StatusBadge status={thread.status} />
          {/* A submittal is a package: the thread plus every revision on it.
              Default is all of them; untick to send just the current one. */}
          <ShareButton ctx={ctx} projectId={project.id} subjectKey={`submittal:${thread.id}`}
            subject={`${thread.name} — ${scope ? scope.name : project.name}`}
            summary={`${thread.revisions.length} revision${thread.revisions.length === 1 ? '' : 's'} · ${thread.status}`}
            items={sorted.map(r => {
              const bytes = approxFileBytes(r.fileUrl);
              return {
                id: r.id,
                label: `Rev. ${r.revisionNumber} — ${r.file || 'no file'}`,
                sub: [r.status, fmtDate(r.date), r.by, bytes ? fmtBytes(bytes) : ''].filter(Boolean).join(' · '),
                bytes, delivery: bytes ? emailDelivery(bytes) : null,
              };
            })} />
          {editable && <Button size="sm" variant="ghost" onClick={() => setRevModal(true)}>+ Add Revision</Button>}
        </div>
      </div>
      {(linkedAppliances.length > 0 || linkedFixtures.length > 0) && (
        <div className="mt-1.5 flex flex-wrap gap-1.5">
          {linkedAppliances.map(i => {
            const spec = ctx.applianceLibrary.find(s => s.id === i.specId);
            return <Badge key={i.id} tone="brown">🔌 {i.label}{spec ? ` — ${spec.manufacturer} ${spec.model}` : ''}</Badge>;
          })}
          {linkedFixtures.map(i => {
            const spec = ctx.fixtureLibrary.find(s => s.id === i.specId);
            return <Badge key={i.id} tone="brown">🚰 {i.label}{spec ? ` — ${spec.manufacturer} ${spec.model}` : ''}</Badge>;
          })}
        </div>
      )}
      <div className="mt-2 space-y-1.5">
        {sorted.map(r => (
          <div key={r.id} className="text-[11px] border-t border-[var(--leon-line)] pt-1.5 first:border-t-0 first:pt-0">
            <div className="flex items-center gap-1.5 flex-wrap">
              <Badge tone={r.status === 'Superseded' ? 'neutral' : statusTone(r.status)}>Rev. {r.revisionNumber} · {r.status}</Badge>
              <span className="text-[var(--leon-black)]/40">{fmtDate(r.date)} · {r.responsiblePerson}</span>
              <span className="flex-1" />
              {/* The revision IS the thing that gets signed — 17 of the 23
                  envelopes this company has sent are exactly this. Sending from
                  here keeps the envelope pointed at the revision it came from,
                  which is what lets the executed copy be filed back on it. */}
              {typeof SignSendButton === 'function' && r.fileUrl && (
                <SignSendButton ctx={ctx} label="Sign"
                  source={{ kind: submittalName || (isResponse ? 'Client Response' : 'Shop Drawing'),
                    refId: r.id, revision: r.revisionNumber, url: r.fileUrl,
                    projectId: project.id, projectName: project.name,
                    scopeId: scope.id, scopeName: scope.name }} />
              )}
            </div>
            <div className="mt-0.5"><FileField name={r.file} url={r.fileUrl} editable={false} onChange={() => {}} /></div>
            {r.notes && <p className="text-[var(--leon-black)]/50 mt-0.5">{r.notes}</p>}
          </div>
        ))}
      </div>
      <AddSubmittalRevisionModal open={revModal} onClose={() => setRevModal(false)} ctx={ctx} project={project} scope={scope} thread={thread} isResponse={isResponse} />
      <RecordDetailModal open={detailOpen} onClose={() => setDetailOpen(false)} title={`${isResponse ? 'Client Response' : 'Shop Drawing / Submittal'} — ${thread.name}`} printable
        fields={[
          { label: 'Project', value: project.name }, { label: 'Scope', value: scope.name }, { label: 'Status', value: thread.status },
          { label: 'Vendor', value: thread.vendorName }, { label: 'Created By', value: thread.createdBy }, { label: 'Created Date', value: fmtDate(thread.createdDate) },
          ...(isResponse && submittalName ? [{ label: 'Responding To', value: `${submittalName} Rev. ${thread.respondingToRevisionNumber}` }] : []),
        ]}
        attachments={sorted.filter(r => r.fileUrl).map(r => ({ name: r.file, url: r.fileUrl }))}
        history={sorted.map(r => ({ id: r.id, date: r.date, user: r.responsiblePerson, reason: `Rev. ${r.revisionNumber} — ${r.status}`, notes: r.notes }))}
      />
    </div>
  );
}
function SubmittalsBlock({ ctx, project, scope, editable }) {
  const [addSubmittal, setAddSubmittal] = useState(false);
  const [addResponse, setAddResponse] = useState(false);
  return (
    <div className="pl-3 border-l-2 border-[var(--leon-line)] ml-2 mt-1">
      <Collapsible title="Shop Drawing / Submittal" count={scope.submittals.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setAddSubmittal(true)}>+ Add Submittal</Button>}>
        {scope.submittals.length === 0 ? <EmptyState text="No submittals yet." /> : scope.submittals.map(t => (
          <SubmittalThreadCard key={t.id} ctx={ctx} project={project} scope={scope} thread={t} isResponse={false} editable={editable} />
        ))}
      </Collapsible>
      <Collapsible title="Client Response / Submittal Response" count={scope.clientResponses.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setAddResponse(true)}>+ Add Response</Button>}>
        {scope.clientResponses.length === 0 ? <EmptyState text="No client responses yet." /> : scope.clientResponses.map(t => (
          <SubmittalThreadCard key={t.id} ctx={ctx} project={project} scope={scope} thread={t} isResponse editable={editable} submittalName={(scope.submittals.find(s => s.id === t.respondingToSubmittalId) || {}).name} />
        ))}
      </Collapsible>
      <AddSubmittalModal open={addSubmittal} onClose={() => setAddSubmittal(false)} ctx={ctx} project={project} scope={scope} />
      <AddClientResponseModal open={addResponse} onClose={() => setAddResponse(false)} ctx={ctx} project={project} scope={scope} />
    </div>
  );
}

// ---- Appliance & Fixture Specifications (§ casework appliance/fixture
// request) — casework-only (appliances) / casework-or-countertop (fixtures),
// each instance links to a reusable library spec instead of re-uploading. --
function SpecInstanceCard({ ctx, project, scope, instance, spec, kind, editable }) {
  const shopDrawing = instance.shopDrawingId ? scope.submittals.find(t => t.id === instance.shopDrawingId) : null;
  const [detailOpen, setDetailOpen] = useState(false);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-2">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="cursor-pointer" onClick={() => setDetailOpen(true)}>
          <p className="text-sm font-bold hover:underline">{instance.label}</p>
          <p className="text-xs text-[var(--leon-black)]/50">
            {spec ? `${kind === 'appliance' ? spec.applianceType : spec.fixtureType} — ${spec.manufacturer} ${spec.model}${spec.modelNumber ? ` (${spec.modelNumber})` : ''}` : 'Spec not found'}
          </p>
        </div>
        {editable && <IconBtn title="Remove link" onClick={() => (kind === 'appliance' ? ctx.removeApplianceInstance : ctx.removeFixtureInstance)(project.id, instance.id)}>✕</IconBtn>}
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/50 mt-1">
        {[instance.unitType, instance.room, instance.cabinetArea].filter(Boolean).join(' · ') || '—'}
        {shopDrawing && <> · Shop Drawing: <strong>{shopDrawing.name}</strong></>}
      </p>
      {spec && (
        <p className="text-[11px] text-[var(--leon-black)]/50 mt-1">
          {spec.finish && <>Finish: {spec.finish} · </>}
          {spec.dimensions && <>Dimensions: {spec.dimensions} · </>}
          {kind === 'appliance' ? (
            <>{spec.voltage && <>Voltage: {spec.voltage} · </>}{spec.gasRequirement && <>Gas: {spec.gasRequirement} · </>}{spec.plumbingRequirement && <>Plumbing: {spec.plumbingRequirement} · </>}{spec.ventilationRequirement && <>Ventilation: {spec.ventilationRequirement}</>}</>
          ) : (
            <>{spec.mountingType && <>Mounting: {spec.mountingType} · </>}{spec.plumbingRequirements && <>Plumbing: {spec.plumbingRequirements} · </>}{spec.cutoutDimensions && <>Cutout: {spec.cutoutDimensions}</>}</>
          )}
        </p>
      )}
      {spec && spec.documents.length > 0 && (
        <div className="flex flex-wrap gap-2 mt-1.5">
          {spec.documents.map(d => <FileField key={d.id} name={`${d.docType}: ${d.name}`} url={d.fileUrl} editable={false} onChange={() => {}} />)}
        </div>
      )}
      {instance.notes && <p className="text-[11px] text-[var(--leon-black)]/50 mt-1">{instance.notes}</p>}
      <RecordDetailModal open={detailOpen} onClose={() => setDetailOpen(false)} title={`${kind === 'appliance' ? 'Appliance' : 'Fixture'} — ${instance.label}`} printable
        fields={[
          { label: 'Type', value: spec ? (kind === 'appliance' ? spec.applianceType : spec.fixtureType) : '—' },
          { label: 'Manufacturer', value: spec?.manufacturer }, { label: 'Model', value: spec?.model }, { label: 'Model #', value: spec?.modelNumber },
          { label: 'Finish', value: spec?.finish }, { label: 'Dimensions', value: spec?.dimensions },
          { label: 'Unit Type', value: instance.unitType }, { label: 'Room', value: instance.room }, { label: 'Cabinet Area', value: instance.cabinetArea },
          { label: 'Shop Drawing', value: shopDrawing ? shopDrawing.name : '—' }, { label: 'Notes', value: instance.notes },
        ]}
        attachments={(spec?.documents || []).map(d => ({ name: `${d.docType}: ${d.name}`, url: d.fileUrl }))}
        relatedRecords={shopDrawing ? [{ label: `Shop Drawing: ${shopDrawing.name}`, onClick: () => ctx.goProjectTab(project.id, 'documents') }] : []}
      />
    </div>
  );
}
function ApplianceSpecsBlock({ ctx, project, scope, editable }) {
  const [showAdd, setShowAdd] = useState(false);
  const instances = project.applianceInstances.filter(i => i.scopeId === scope.id);
  return (
    <div className="pl-3 border-l-2 border-[var(--leon-line)] ml-2 mt-1">
      <Collapsible title="Appliance Specifications" count={instances.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add Appliance</Button>}>
        {instances.length === 0 ? <EmptyState text="No appliance specifications linked yet." /> : instances.map(i => (
          <SpecInstanceCard key={i.id} ctx={ctx} project={project} scope={scope} instance={i} spec={ctx.applianceLibrary.find(s => s.id === i.specId)} kind="appliance" editable={editable} />
        ))}
      </Collapsible>
      <AddSpecInstanceModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} scope={scope} kind="appliance" />
    </div>
  );
}
function FixtureSpecsBlock({ ctx, project, scope, editable }) {
  const [showAdd, setShowAdd] = useState(false);
  const instances = project.fixtureInstances.filter(i => i.scopeId === scope.id || i.countertopScopeId === scope.id);
  return (
    <div className="pl-3 border-l-2 border-[var(--leon-line)] ml-2 mt-1">
      <Collapsible title="Fixture Specifications" count={instances.length} right={editable && scope.familyName === 'Casework' && <Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add Fixture</Button>}>
        {instances.length === 0 ? <EmptyState text="No fixture specifications linked yet." /> : instances.map(i => (
          <SpecInstanceCard key={i.id} ctx={ctx} project={project} scope={scope} instance={i} spec={ctx.fixtureLibrary.find(s => s.id === i.specId)} kind="fixture" editable={editable} />
        ))}
      </Collapsible>
      <AddSpecInstanceModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} scope={scope} kind="fixture" />
    </div>
  );
}
function AddSpecInstanceModal({ open, onClose, ctx, project, scope, kind }) {
  const isAppliance = kind === 'appliance';
  const library = isAppliance ? ctx.applianceLibrary : ctx.fixtureLibrary;
  const types = isAppliance ? APPLIANCE_TYPES : FIXTURE_TYPES;
  const blankInstance = { label: '', unitType: '', room: '', cabinetArea: '', countertopScopeId: '', shopDrawingId: '', notes: '' };
  const blankSpec = isAppliance
    ? { applianceType: types[0], manufacturer: '', model: '', modelNumber: '', finish: '', dimensions: '', voltage: '', gasRequirement: '', plumbingRequirement: '', ventilationRequirement: '', notes: '' }
    : { fixtureType: types[0], manufacturer: '', model: '', modelNumber: '', finish: '', dimensions: '', mountingType: '', plumbingRequirements: '', cutoutDimensions: '', notes: '' };
  const [mode, setMode] = useState('existing');
  const [specId, setSpecId] = useState('');
  const [specForm, setSpecForm] = useState(blankSpec);
  const [docs, setDocs] = useState({});
  const [form, setForm] = useState(blankInstance);
  const countertopScopes = project.scopes.filter(s => s.familyName === 'Countertop');
  useEffect(() => {
    if (open) {
      setMode(library.length ? 'existing' : 'new');
      setSpecId(library[0]?.id || '');
      setSpecForm(blankSpec);
      setDocs({});
      setForm(blankInstance);
    }
  }, [open]);
  function submit() {
    let finalSpecId = specId;
    if (mode === 'new') {
      const created = isAppliance ? ctx.addApplianceSpec(specForm) : ctx.addFixtureSpec(specForm);
      finalSpecId = created.id;
      SPEC_DOC_TYPES.forEach(docType => {
        const d = docs[docType];
        if (d && d.fileUrl) {
          if (isAppliance) ctx.addApplianceSpecDocument(created.id, docType, d.file, d.fileUrl);
          else ctx.addFixtureSpecDocument(created.id, docType, d.file, d.fileUrl);
        }
      });
    }
    if (!finalSpecId || !form.label.trim()) return;
    const data = { ...form, scopeId: scope.id, specId: finalSpecId, shopDrawingId: form.shopDrawingId || null, countertopScopeId: form.countertopScopeId || null };
    if (isAppliance) ctx.addApplianceInstance(project.id, data);
    else ctx.addFixtureInstance(project.id, data);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Add ${isAppliance ? 'Appliance' : 'Fixture'} Specification`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Link {isAppliance ? 'Appliance' : 'Fixture'}</Button></>}>
      <div className="space-y-4">
        <div className="flex gap-1 border-b border-[var(--leon-line)]">
          <button onClick={() => setMode('existing')} className={`px-3 py-1.5 text-sm font-semibold border-b-2 ${mode === 'existing' ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50'}`}>Use Existing Spec</button>
          <button onClick={() => setMode('new')} className={`px-3 py-1.5 text-sm font-semibold border-b-2 ${mode === 'new' ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50'}`}>Add New Spec</button>
        </div>

        {mode === 'existing' ? (
          library.length === 0 ? <EmptyState text={`No ${isAppliance ? 'appliance' : 'fixture'} specs in the library yet — add a new one.`} /> : (
            <Field label={isAppliance ? 'Appliance Spec' : 'Fixture Spec'}>
              <Select value={specId} onChange={e => setSpecId(e.target.value)}>
                {library.map(s => <option key={s.id} value={s.id}>{isAppliance ? s.applianceType : s.fixtureType} — {s.manufacturer} {s.model}{s.modelNumber ? ` (${s.modelNumber})` : ''}</option>)}
              </Select>
            </Field>
          )
        ) : (
          <div className="space-y-3">
            <div className="grid grid-cols-3 gap-3">
              <Field label={isAppliance ? 'Appliance Type' : 'Fixture Type'}>
                <Select value={isAppliance ? specForm.applianceType : specForm.fixtureType} onChange={e => setSpecForm({ ...specForm, [isAppliance ? 'applianceType' : 'fixtureType']: e.target.value })}>
                  {types.map(t => <option key={t}>{t}</option>)}
                </Select>
              </Field>
              <Field label="Manufacturer"><TextInput value={specForm.manufacturer} onChange={e => setSpecForm({ ...specForm, manufacturer: e.target.value })} /></Field>
              <Field label="Model"><TextInput value={specForm.model} onChange={e => setSpecForm({ ...specForm, model: e.target.value })} /></Field>
            </div>
            <div className="grid grid-cols-3 gap-3">
              <Field label="Model Number"><TextInput value={specForm.modelNumber} onChange={e => setSpecForm({ ...specForm, modelNumber: e.target.value })} /></Field>
              <Field label="Finish / Color"><TextInput value={specForm.finish} onChange={e => setSpecForm({ ...specForm, finish: e.target.value })} /></Field>
              <Field label="Dimensions"><TextInput value={specForm.dimensions} onChange={e => setSpecForm({ ...specForm, dimensions: e.target.value })} /></Field>
            </div>
            {isAppliance ? (
              <div className="grid grid-cols-2 gap-3">
                <Field label="Voltage / Electrical"><TextInput value={specForm.voltage} onChange={e => setSpecForm({ ...specForm, voltage: e.target.value })} /></Field>
                <Field label="Gas Requirement"><TextInput value={specForm.gasRequirement} onChange={e => setSpecForm({ ...specForm, gasRequirement: e.target.value })} /></Field>
                <Field label="Plumbing Requirement"><TextInput value={specForm.plumbingRequirement} onChange={e => setSpecForm({ ...specForm, plumbingRequirement: e.target.value })} /></Field>
                <Field label="Ventilation Requirement"><TextInput value={specForm.ventilationRequirement} onChange={e => setSpecForm({ ...specForm, ventilationRequirement: e.target.value })} /></Field>
              </div>
            ) : (
              <div className="grid grid-cols-3 gap-3">
                <Field label="Mounting Type"><TextInput value={specForm.mountingType} onChange={e => setSpecForm({ ...specForm, mountingType: e.target.value })} /></Field>
                <Field label="Plumbing Requirements"><TextInput value={specForm.plumbingRequirements} onChange={e => setSpecForm({ ...specForm, plumbingRequirements: e.target.value })} /></Field>
                <Field label="Cutout Dimensions"><TextInput value={specForm.cutoutDimensions} onChange={e => setSpecForm({ ...specForm, cutoutDimensions: e.target.value })} /></Field>
              </div>
            )}
            <Field label="Notes"><TextArea rows={2} value={specForm.notes} onChange={e => setSpecForm({ ...specForm, notes: e.target.value })} /></Field>
            <div className="grid grid-cols-3 gap-3">
              {SPEC_DOC_TYPES.map(docType => (
                <Field key={docType} label={docType}>
                  <FileField name={(docs[docType] || {}).file} url={(docs[docType] || {}).fileUrl} editable onChange={(fname, url) => setDocs(d => ({ ...d, [docType]: { file: fname, fileUrl: url } }))} />
                </Field>
              ))}
            </div>
          </div>
        )}

        <div className="border-t border-[var(--leon-line)] pt-3 space-y-3">
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">Link to this project</p>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Label" hint='e.g. "Refrigerator 01" or "Dishwasher - Unit Type A"'><TextInput value={form.label} onChange={e => setForm({ ...form, label: e.target.value })} /></Field>
            <Field label="Unit Type"><TextInput value={form.unitType} onChange={e => setForm({ ...form, unitType: e.target.value })} /></Field>
          </div>
          <div className="grid grid-cols-2 gap-3">
            <Field label="Room"><TextInput value={form.room} onChange={e => setForm({ ...form, room: e.target.value })} /></Field>
            <Field label="Cabinet / Casework Area"><TextInput value={form.cabinetArea} onChange={e => setForm({ ...form, cabinetArea: e.target.value })} /></Field>
          </div>
          {!isAppliance && scope.familyName === 'Casework' && countertopScopes.length > 0 && (
            <Field label="Also Affects Countertop Scope" hint="Optional — links this fixture to the related countertop cutout too">
              <Select value={form.countertopScopeId} onChange={e => setForm({ ...form, countertopScopeId: e.target.value })}>
                <option value="">— none —</option>
                {countertopScopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
              </Select>
            </Field>
          )}
          <Field label="Shop Drawing" hint="Optional — shows this spec on that submittal's card">
            <Select value={form.shopDrawingId} onChange={e => setForm({ ...form, shopDrawingId: e.target.value })}>
              <option value="">— none —</option>
              {scope.submittals.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
            </Select>
          </Field>
          <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        </div>
      </div>
    </Modal>
  );
}

function AddSubmittalModal({ open, onClose, ctx, project, scope }) {
  const blank = { name: '', vendorId: '', date: todayISO(), status: 'Submitted', file: '', fileUrl: null, notes: '', responsiblePerson: ctx.currentUserName };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, responsiblePerson: ctx.currentUserName }); }, [open]);
  function submit() {
    if (!form.name.trim() || !form.file) return;
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    ctx.addSubmittal(project.id, scope.id, 'Shop Drawing / Submittal', { ...form, vendorName: vendor ? vendor.name : '' });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Submittal — ${scope.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Submittal</Button></>}>
      <div className="space-y-3">
        <Field label="Document Name / Number"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. Kitchen Cabinetry — Shop Drawing Submittal" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Vendor / Manufacturer"><Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}><option value="">—</option>{ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</Select></Field>
          <Field label="Submission Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{SUBMITTAL_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
          <Field label="Responsible Person"><TextInput value={form.responsiblePerson} onChange={e => setForm({ ...form, responsiblePerson: e.target.value })} /></Field>
        </div>
        <Field label="Attachment (Rev. 0)"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddClientResponseModal({ open, onClose, ctx, project, scope }) {
  const allRevisions = scope.submittals.flatMap(s => s.revisions.map(r => ({ submittal: s, revision: r })));
  const blank = { targetKey: allRevisions[0] ? `${allRevisions[0].submittal.id}::${allRevisions[0].revision.revisionNumber}` : '', name: '', respondentCompany: '', date: todayISO(), status: 'Awaiting Response', file: '', fileUrl: null, notes: '', responsiblePerson: ctx.currentUserName };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, responsiblePerson: ctx.currentUserName, targetKey: allRevisions[0] ? `${allRevisions[0].submittal.id}::${allRevisions[0].revision.revisionNumber}` : '' }); }, [open]);
  if (allRevisions.length === 0) {
    return (
      <Modal open={open} onClose={onClose} title="Add Client Response">
        <EmptyState text="Add a Shop Drawing / Submittal first — a response must link to a specific submittal revision." />
      </Modal>
    );
  }
  function submit() {
    if (!form.name.trim() || !form.file || !form.targetKey) return;
    const [submittalId, revNumStr] = form.targetKey.split('::');
    ctx.addClientResponse(project.id, scope.id, submittalId, Number(revNumStr), { ...form, vendorName: form.respondentCompany });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Client Response — ${scope.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Response</Button></>}>
      <div className="space-y-3">
        <Field label="Responding To" hint="The exact Shop Drawing / Submittal revision this response answers.">
          <Select value={form.targetKey} onChange={e => setForm({ ...form, targetKey: e.target.value })}>
            {allRevisions.map(({ submittal, revision }) => (
              <option key={`${submittal.id}::${revision.revisionNumber}`} value={`${submittal.id}::${revision.revisionNumber}`}>{submittal.name} — Rev. {revision.revisionNumber}</option>
            ))}
          </Select>
        </Field>
        <Field label="Document Name / Number"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. Architect Markup — Kitchen Shop Drawing" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Received From" hint="Client / architect / GC / consultant"><TextInput value={form.respondentCompany} onChange={e => setForm({ ...form, respondentCompany: e.target.value })} /></Field>
          <Field label="Response Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{SUBMITTAL_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
          <Field label="Responsible Person"><TextInput value={form.responsiblePerson} onChange={e => setForm({ ...form, responsiblePerson: e.target.value })} /></Field>
        </div>
        <Field label="Attachment (Rev. 0)"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddSubmittalRevisionModal({ open, onClose, ctx, project, scope, thread, isResponse }) {
  const blank = { date: todayISO(), status: isResponse ? 'Awaiting Response' : 'Submitted', file: '', fileUrl: null, notes: '', responsiblePerson: ctx.currentUserName };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, responsiblePerson: ctx.currentUserName }); }, [open]);
  if (!thread) return null;
  function submit() {
    if (!form.file) return;
    ctx.addSubmittalRevision(project.id, scope.id, thread.id, isResponse, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Revision — ${thread.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Revision</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">This creates a new revision — the prior one is preserved and marked Superseded, never overwritten.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{SUBMITTAL_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
        </div>
        <Field label="Responsible Person"><TextInput value={form.responsiblePerson} onChange={e => setForm({ ...form, responsiblePerson: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Automatic Submittal Package (§3, §22) ---------------------------------
function scopeLinkedMaterials(ctx, scope) {
  const ids = [...new Set(Object.values(scope.materialLinks || {}).filter(Boolean))];
  return ids.map(id => ctx.materialLibrary.find(m => m.id === id)).filter(Boolean);
}
function SubmittalPackageModal({ open, onClose, ctx, project, scope }) {
  const materials = scopeLinkedMaterials(ctx, scope);
  const latestSubmittal = scope.submittals.length ? latestProductionDoc(scope.submittals.flatMap(s => s.revisions.map(r => ({ ...r, threadName: s.name })))) : null;
  const PACKAGE_ITEMS = [
    { key: 'materialSpec', label: 'Material Specification', available: materials.length > 0 },
    { key: 'shopDrawing', label: 'Shop Drawing', available: scope.submittals.length > 0 },
    { key: 'techData', label: 'Technical Data Sheet', available: materials.some(m => m.documents.some(d => d.docType === 'Technical Data Sheet')) },
    { key: 'careMaintenance', label: 'Care & Maintenance', available: materials.some(m => m.documents.some(d => d.docType === 'Care & Maintenance' || d.docType === 'Cleaning Instructions')) },
    { key: 'warranty', label: 'Warranty', available: materials.some(m => m.documents.some(d => d.docType === 'Manufacturer Warranty' || d.docType === 'Product Warranty')) },
    { key: 'installation', label: 'Installation Instructions', available: materials.some(m => m.documents.some(d => d.docType === 'Installation Instructions')) },
    { key: 'certifications', label: 'Certifications', available: materials.some(m => m.documents.some(d => d.docType === 'Safety / Technical Certification')) },
    { key: 'samples', label: 'Samples Information', available: false },
    { key: 'other', label: 'Other Supporting Documents', available: materials.some(m => m.documents.some(d => d.docType === 'Other Product Documentation')) },
  ];
  const [checked, setChecked] = useState({});
  const [preview, setPreview] = useState(false);
  // preview's print-only block lives as a sibling of <Modal> (so it survives
  // being inside a closed modal — same reason as every other print fix this
  // session). DocumentsTab mounts one SubmittalPackageModal PER SCOPE,
  // unconditionally, so every scope's instance stays mounted (and keeps its
  // own preview state) for as long as the Documents tab is open — closing
  // the visible dialog only happens implicitly (preview=true hides it), so
  // the user has no "Cancel" to click that would reset it. A stale
  // preview=true from a PREVIOUSLY generated package therefore stayed in the
  // DOM indefinitely and stacked into every later scope's print — the
  // reported "prints all scopes" bug. `.print-only` is a global CSS rule
  // (styles.css) that shows ANY element with that class when printing, so
  // any leftover instance bleeds into an unrelated later print job. Resetting
  // on open/close only fixes repeat-use of the SAME instance; the actual fix
  // is resetting every instance's preview after the print dialog is dismissed
  // (afterprint fires on both print and cancel), which is the only reliable
  // signal that a print job — real or accumulated — has been fully consumed.
  useEffect(() => { setPreview(false); if (open) setChecked(Object.fromEntries(PACKAGE_ITEMS.filter(i => i.available).map(i => [i.key, true]))); }, [open]);
  useEffect(() => {
    function resetAfterPrint() { setPreview(false); }
    window.addEventListener('afterprint', resetAfterPrint);
    return () => window.removeEventListener('afterprint', resetAfterPrint);
  }, []);
  function toggle(key) { setChecked(c => ({ ...c, [key]: !c[key] })); }
  function printPackage() { setPreview(true); setTimeout(() => window.print(), 50); }

  const docTypeMap = { techData: 'Technical Data Sheet', careMaintenance: ['Care & Maintenance', 'Cleaning Instructions'], warranty: ['Manufacturer Warranty', 'Product Warranty'], installation: 'Installation Instructions', certifications: 'Safety / Technical Certification', other: 'Other Product Documentation' };

  return (
    <>
      <Modal wide open={open && !preview} onClose={onClose} title={`Generate Submittal Package — ${scope.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={printPackage}>Generate Package</Button></>}>
        <div className="space-y-3">
          <p className="text-xs text-[var(--leon-black)]/50">Documents already on file for materials linked to this scope are pre-selected automatically. Choose which to include — the package opens as one combined, printable document.</p>
          <p className="text-xs font-semibold uppercase tracking-wide text-[var(--leon-black)]/50">Include in Submittal Package:</p>
          <div className="space-y-1.5">
            {PACKAGE_ITEMS.map(item => (
              <label key={item.key} className={`flex items-center gap-2 text-sm ${!item.available ? 'opacity-40' : ''}`}>
                <input type="checkbox" checked={!!checked[item.key]} disabled={!item.available} onChange={() => toggle(item.key)} />
                {item.label} {!item.available && <span className="text-[10px]">(none on file)</span>}
              </label>
            ))}
          </div>
        </div>
      </Modal>
      {preview && (
        <div className="print-only print-area p-8">
          <PrintHeader project={project} account={ctx.accounts.find(a => a.id === project.accountId)} title={`Submittal Package — ${scope.name}`} />
          {checked.materialSpec && materials.map(m => (
            <div key={m.id} className="mb-4 break-inside-avoid">
              <h3 className="font-bold text-base border-b border-black pb-1 mb-1">Material Specification — {m.name}</h3>
              <p className="text-sm text-gray-600 mb-1">{m.category} · {m.manufacturer}{m.productCode ? ` · #${m.productCode}` : ''}</p>
              <table className="w-full text-sm"><tbody>
                {(MATERIAL_FIELD_DEFS[m.category] || []).filter(f => m.specs[f.key]).map(f => (
                  <tr key={f.key} className="border-b border-gray-200"><td className="py-1 pr-4 font-semibold w-1/3">{f.label}</td><td className="py-1">{m.specs[f.key]}</td></tr>
                ))}
              </tbody></table>
            </div>
          ))}
          {checked.shopDrawing && latestSubmittal && (
            <div className="mb-4 break-inside-avoid">
              <h3 className="font-bold text-base border-b border-black pb-1 mb-1">Shop Drawing — {latestSubmittal.threadName}, Rev. {latestSubmittal.revisionNumber}</h3>
              <p className="text-sm">{latestSubmittal.file || 'No attachment on file.'}</p>
            </div>
          )}
          {['techData', 'careMaintenance', 'warranty', 'installation', 'certifications', 'other'].filter(k => checked[k]).map(key => {
            const types = [].concat(docTypeMap[key]);
            const docs = materials.flatMap(m => m.documents.filter(d => types.includes(d.docType)).map(d => ({ ...d, materialName: m.name })));
            if (docs.length === 0) return null;
            return (
              <div key={key} className="mb-4 break-inside-avoid">
                <h3 className="font-bold text-base border-b border-black pb-1 mb-1">{PACKAGE_ITEMS.find(i => i.key === key).label}</h3>
                <ul className="text-sm list-disc pl-5">
                  {docs.map(d => <li key={d.id}>{d.materialName} — {d.file}</li>)}
                </ul>
              </div>
            );
          })}
        </div>
      )}
    </>
  );
}
// `target` is { scope, sectionId?, sectionName? } — a document added from
// inside a custom section is filed there, not in the scope's general list.
function AddDocumentModal({ open, target, onClose, ctx, project }) {
  const scope = target && target.scope;
  const [form, setForm] = useState({ name: '', fileUrl: null, type: SCOPE_DOC_TYPES[0], revision: 1, dateCreated: todayISO(), sharedWithClient: false });
  useEffect(() => { if (open) setForm({ name: '', fileUrl: null, type: SCOPE_DOC_TYPES[0], revision: 1, dateCreated: todayISO(), sharedWithClient: false }); }, [open]);
  if (!scope) return null;
  function submit() {
    if (!form.name.trim()) return;
    ctx.addDocument(project.id, scope.id, { ...form, revision: Number(form.revision), sectionId: (target && target.sectionId) || null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Document — ${scope.name}${target && target.sectionName ? ` · ${target.sectionName}` : ''}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Attachment"><FileField name={form.name} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, name: fname, fileUrl: url })} editable /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Type"><Select value={form.type} onChange={e => setForm({ ...form, type: e.target.value })}>{SCOPE_DOC_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
          <Field label="Revision"><TextInput type="number" min="1" value={form.revision} onChange={e => setForm({ ...form, revision: e.target.value })} /></Field>
        </div>
        <Field label="Date Created"><TextInput type="date" value={form.dateCreated} onChange={e => setForm({ ...form, dateCreated: e.target.value })} /></Field>
        <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={form.sharedWithClient} onChange={e => setForm({ ...form, sharedWithClient: e.target.checked })} /> Shared with client</label>
      </div>
    </Modal>
  );
}

// ---- Procurement (vendor estimates -> POs) ---------------------------------
// Allocating stock to THIS job, from inside the job. The same records the
// Inventory hub writes — one allocation list, not a second one — but reached
// from where the need actually arises, and able to take several materials in
// one go rather than one modal per line.
//
// It shows AVAILABLE, not stock on hand: what is already promised to another
// job is not yours to take, and a picker that shows the larger number invites
// an allocation the engine will then refuse.
function AllocateMaterialModal({ open, onClose, ctx, project }) {
  const blank = { q: '', scopeId: '', building: '', floor: '', unit: '', requiredDate: '', notes: '' };
  const [f, setF] = useState(blank);
  const [picked, setPicked] = useState({});      // materialId -> qty typed
  const [error, setError] = useState('');
  useEffect(() => { if (open) { setF(blank); setPicked({}); setError(''); } }, [open]);

  const scopes = ctx.deptScopes(project);
  const q = f.q.trim().toLowerCase();
  const rows = (ctx.warehouseMaterials || [])
    .filter(m => m.active !== false)
    .map(m => ({ m, avail: availableQuantity(m, ctx.materialAllocations) }))
    .filter(r => !q || (r.m.name + ' ' + (r.m.category || '') + ' ' + (r.m.referenceNumber || '')).toLowerCase().includes(q));
  const chosen = Object.entries(picked).filter(([, v]) => Number(v) > 0);

  function submit() {
    setError('');
    const problems = [];
    let made = 0;
    chosen.forEach(([materialId, qty]) => {
      const res = ctx.addMaterialAllocation({
        materialId, projectId: project.id, scopeId: f.scopeId || null,
        quantityAllocated: Number(qty),
        unitOfMeasure: (ctx.warehouseMaterials.find(m => m.id === materialId) || {}).unitOfMeasure || '',
        building: f.building, floor: f.floor, unit: f.unit,
        requiredDate: f.requiredDate || null,
        notes: f.notes,
      });
      if (res && res.error) problems.push(res.error); else made++;
    });
    // Partial success is reported rather than hidden: some lines can be refused
    // for want of stock while the rest go through, and saying "done" would be
    // a lie about the ones that did not.
    if (problems.length) {
      setError(`${made} allocated. ${problems.length} could not be: ${problems.join(' ')}`);
      setPicked({});
      return;
    }
    onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Allocate material to this job"
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <div className="flex-1" />
               <Button disabled={!chosen.length} onClick={submit}>
                 {chosen.length ? `Allocate ${chosen.length} material${chosen.length === 1 ? '' : 's'}` : 'Allocate'}
               </Button></>}>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Scope" hint="Allocated cost lands on this scope. Leave blank to allocate to the job.">
            <Select value={f.scopeId} onChange={e => setF({ ...f, scopeId: e.target.value })}>
              <option value="">— the job as a whole —</option>
              {scopes.map(sc => <option key={sc.id} value={sc.id}>{sc.name}</option>)}
            </Select>
          </Field>
          <Field label="Needed by">
            <TextInput type="date" value={f.requiredDate} onChange={e => setF({ ...f, requiredDate: e.target.value })} />
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Building"><TextInput value={f.building} onChange={e => setF({ ...f, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={f.floor} onChange={e => setF({ ...f, floor: e.target.value })} /></Field>
          <Field label="Unit"><TextInput value={f.unit} onChange={e => setF({ ...f, unit: e.target.value })} /></Field>
        </div>

        {/* One note for the whole allocation. Written onto every material taken
            in this go, because they are being taken for the same reason — the
            reason is what someone reads the note for later. */}
        <Field label="Note" hint="Why this material is being taken, in your words. Goes on every line allocated here.">
          <TextInput value={f.notes} onChange={e => setF({ ...f, notes: e.target.value })}
            placeholder="e.g. first-floor bathrooms, ahead of the Tuesday delivery" />
        </Field>

        <Field label="Find the material" hint="Type a quantity against everything you need — several at once is fine.">
          <TextInput placeholder="Name, category or item no.…" value={f.q} onChange={e => setF({ ...f, q: e.target.value })} />
        </Field>

        <div className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
          <div className="max-h-72 overflow-y-auto">
            <table className="w-full text-xs">
              <thead className="bg-[var(--leon-cream)] sticky top-0">
                <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
                  <th className="px-3 py-2"></th><th className="px-3 py-2">Material</th>
                  <th className="px-3 py-2">Category</th><th className="px-3 py-2">Available</th>
                  <th className="px-3 py-2 w-28">Allocate</th>
                </tr>
              </thead>
              <tbody>
                {rows.length === 0 && (
                  <tr><td colSpan={5} className="px-3 py-6 text-center text-[var(--leon-black)]/40 italic">Nothing matches.</td></tr>
                )}
                {rows.map(({ m, avail }) => (
                  <tr key={m.id} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-1.5"><MaterialThumb material={m} /></td>
                    <td className="px-3 py-1.5 font-semibold">{m.name}</td>
                    <td className="px-3 py-1.5 text-[var(--leon-black)]/55">{m.category || '—'}</td>
                    <td className={`px-3 py-1.5 tabular-nums ${avail <= 0 ? 'text-[var(--leon-black)]/35' : ''}`}>
                      {avail} {m.unitOfMeasure}
                    </td>
                    <td className="px-3 py-1.5">
                      <TextInput type="number" min="0" max={avail} disabled={avail <= 0}
                        className="!w-24 !py-1 !text-xs"
                        value={picked[m.id] || ''}
                        onChange={e => setPicked(p => ({ ...p, [m.id]: e.target.value }))} />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        {error && <p className="text-xs text-[var(--leon-red)]">{error}</p>}
        <p className="text-[11px] text-[var(--leon-black)]/50">
          <strong>Available</strong> is stock on hand less what is already promised to another job. This
          writes the same allocations the Inventory hub does &mdash; one list, reached from two places.
        </p>
      </div>
    </Modal>
  );
}
function ProcurementTab({ ctx, project }) {
  const editable = ctx.canEdit('procurement');
  const [showAdd, setShowAdd] = useState(false);
  const [revisionFor, setRevisionFor] = useState(null);
  const [allocDetailFor, setAllocDetailFor] = useState(null);
  const [allocating, setAllocating] = useState(false);
  const [requestDeliveryFor, setRequestDeliveryFor] = useState(null);
  const [estimateDetailFor, setEstimateDetailFor] = useState(null);
  const canPM = ['Project Coordinator', 'General Manager', 'Production Director', 'Admin'].includes(ctx.currentRole);
  const canOwner = ['Admin', 'Accounting'].includes(ctx.currentRole);

  // scopeId === null means "every scope" (the Overview subtab); a real
  // scopeId filters every one of the four sections below to just that
  // scope's estimates/POs/PIs/allocated material, per explicit request.
  function ProcurementScopeBlock({ scopeId }) {
    const vendorEstimates = project.vendorEstimates.filter(ve => scopeId === null || ve.scopeId === scopeId);
    const purchaseOrders = project.purchaseOrders.filter(po => scopeId === null || po.scopeId === scopeId);
    const proformaInvoices = project.proformaInvoices.filter(pi => pi.partyType === 'Vendor' && (scopeId === null || pi.scopeId === scopeId));
    const allocations = ctx.materialAllocations.filter(a => a.projectId === project.id && (scopeId === null || a.scopeId === scopeId));
    return (
      <div>
      <Collapsible title="Vendor Estimates" count={vendorEstimates.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add Estimate</Button>}>
        {vendorEstimates.length === 0 ? <EmptyState text="No vendor estimates." /> : (
          <div className="space-y-2">
            {vendorEstimates.map(ve => (
              <div key={ve.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap">
                  <div className="cursor-pointer" onClick={() => setEstimateDetailFor(ve)}>
                    <p className="text-sm font-bold hover:underline">
                      <Badge tone="black">{ve.estimateNumber}</Badge> {ve.vendorName} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(ve.amount)} (Rev {ve.revisions[ve.revisions.length - 1]?.revision || 1})</span>
                    </p>
                    <div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
                      <StatusBadge status={ve.status} />
                      <Badge tone="neutral">{ve.category || 'Original Order'}</Badge>
                      {ve.scopeId && (() => { const s = project.scopes.find(x => x.id === ve.scopeId); return s ? <Badge tone="neutral">{s.name}</Badge> : null; })()}
                      {isUnplannedCost(ve.category) && <Badge tone={ve.recoverability === 'Business Loss' ? 'red' : 'yellow'}>⚠ Unplanned{ve.recoverability ? ` — ${ve.recoverability}` : ''}</Badge>}
                    </div>
                    <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{ve.description} · {fmtDate(ve.date)}{ve.unplannedReason ? ` · Cause: ${ve.unplannedReason}` : ''}</p>
                  </div>
                  {ve.poId && <Badge tone="green">PO Issued{(() => { const po = project.purchaseOrders.find(p => p.id === ve.poId); return po?.poNumber ? ` — ${po.poNumber}` : ''; })()}</Badge>}
                </div>
                <div className="flex items-center gap-4 mt-2 text-xs flex-wrap">
                  <ApprovalPill label="Project Mgmt" approved={ve.pmApproved} by={ve.pmApprovedBy} date={ve.pmApprovedDate}
                    onApprove={canPM && !ve.pmApproved ? () => ctx.approveVendor(project.id, ve.id, 'pm') : null} />
                  <ApprovalPill label="Ownership" approved={ve.ownerApproved} by={ve.ownerApprovedBy} date={ve.ownerApprovedDate}
                    onApprove={canOwner && !ve.ownerApproved ? () => ctx.approveVendor(project.id, ve.id, 'owner') : null} />
                  {editable && <Button size="sm" variant="ghost" onClick={() => setRevisionFor(ve)}>+ Add Revision</Button>}
                </div>
                {ve.revisions.length > 0 && (
                  <div className="mt-2 pt-2 border-t border-[var(--leon-line)] space-y-1">
                    {ve.revisions.map(r => (
                      <p key={r.id} className="text-[11px] text-[var(--leon-black)]/50 flex items-center gap-1.5 flex-wrap">
                        Rev {r.revision}: {fmtMoney(r.amount)} · {fmtDate(r.date)}
                        {(r.file || editable) && <FileField name={r.file} url={r.fileUrl} onChange={(fname, url) => ctx.updateVendorEstimateRevision(project.id, ve.id, r.id, { file: fname, fileUrl: url })} editable={editable} />}
                        {r.note ? ` — ${r.note}` : ''}
                      </p>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </div>
        )}
      </Collapsible>

      <Collapsible title="Purchase Orders (Payables)" count={purchaseOrders.length}>
        {purchaseOrders.length === 0 ? <EmptyState text="No POs issued yet." /> : (
          <div className="space-y-2">
            {purchaseOrders.map(po => <PoCard key={po.id} ctx={ctx} project={project} po={po} partyType="Vendor" editable={editable} />)}
          </div>
        )}
      </Collapsible>

      <Collapsible title="Proforma Invoices" count={proformaInvoices.length}>
        {proformaInvoices.length === 0 ? <EmptyState text="No proforma invoices yet — convert an approved PO above." /> : (
          <div className="space-y-2">
            {proformaInvoices.map(pi => <PiCard key={pi.id} ctx={ctx} project={project} pi={pi} editable={editable} />)}
          </div>
        )}
      </Collapsible>

      <Collapsible title="Material Allocations" count={allocations.length}
        right={ctx.canAllocateMaterial
          ? <Button size="sm" variant="ghost" onClick={e => { e.stopPropagation(); setAllocating(true); }}>+ Allocate material</Button>
          : null}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-1.5">Click an allocation to request its delivery.</p>
        {allocations.length === 0 ? <EmptyState text="No material allocated to this job yet — use “Allocate material” above." /> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-2">Material</th><th className="py-1 pr-2">Location</th><th className="py-1 pr-2">Qty Allocated</th><th className="py-1 pr-2">Released</th><th className="py-1 pr-2">Delivered</th><th className="py-1 pr-2">Status</th><th className="py-1 pr-2">Required</th><th className="py-1 pr-2"></th></tr></thead>
              <tbody>
                {allocations.map(a => {
                  const material = ctx.warehouseMaterials.find(m => m.id === a.materialId);
                  const requestable = ['Reserved', 'Partially Released'].includes(a.status);
                  return (
                    <tr key={a.id} className="border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => requestable ? setRequestDeliveryFor(a) : setAllocDetailFor(a)}>
                      <td className="py-1.5 pr-2 font-semibold hover:underline">{material ? material.name : '—'}</td>
                      <td className="py-1.5 pr-2">{[a.building, a.floor, a.unit].filter(Boolean).join(' · ') || '—'}</td>
                      <td className="py-1.5 pr-2">{a.quantityAllocated} {a.unitOfMeasure}</td>
                      <td className="py-1.5 pr-2">{a.quantityReleased} {a.unitOfMeasure}</td>
                      <td className="py-1.5 pr-2">{a.quantityDelivered} {a.unitOfMeasure}</td>
                      <td className="py-1.5 pr-2"><StatusBadge status={a.status} /></td>
                      <td className="py-1.5 pr-2">{fmtDate(a.requiredDate)}</td>
                      <td className="py-1.5 pr-2 whitespace-nowrap">
                        {requestable ? <Button size="sm" variant="ghost" onClick={e => { e.stopPropagation(); setRequestDeliveryFor(a); }}>Request Delivery</Button> : <button onClick={e => { e.stopPropagation(); setAllocDetailFor(a); }} className="text-[var(--leon-brown)] font-semibold">History</button>}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Collapsible>
      </div>
    );
  }

  return (
    <div>
      <ScopeHubTabs ctx={ctx} project={project} overview={<ProcurementScopeBlock scopeId={null} />} renderScope={scope => <ProcurementScopeBlock scopeId={scope.id} />} />
      <AllocateMaterialModal open={allocating} onClose={() => setAllocating(false)} ctx={ctx} project={project} />
      <AllocationDetailModal open={!!allocDetailFor} allocation={allocDetailFor} onClose={() => setAllocDetailFor(null)} ctx={ctx} />
      <RequestDeliveryModal open={!!requestDeliveryFor} onClose={() => setRequestDeliveryFor(null)} ctx={ctx} project={project} preselectedAllocationId={requestDeliveryFor?.id} />

      <AddVendorEstimateModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <AddVendorEstimateRevisionModal open={!!revisionFor} ve={revisionFor} onClose={() => setRevisionFor(null)} ctx={ctx} project={project} />
      <EstimateDetailModal open={!!estimateDetailFor} estimate={estimateDetailFor} partyType="Vendor" onClose={() => setEstimateDetailFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function ApprovalPill({ label, approved, by, date, onApprove }) {
  return (
    <div className="flex items-center gap-1.5">
      {approved ? <Badge tone="green">✓ {label}</Badge> : <Badge tone="yellow">Pending {label}</Badge>}
      {approved && by && <span className="text-[10px] text-[var(--leon-black)]/40">{by}, {fmtDate(date)}</span>}
      {onApprove && <Button size="sm" onClick={onApprove}>Approve</Button>}
    </div>
  );
}

// ---- Estimate -> PO -> Proforma Invoice shared chain UI (§ procurement
// chain request) — used by both ProcurementTab (vendor) and ExportTab
// (freight); every revision is append-only, nothing here overwrites history.
function RevisionHistoryList({ revisions, editable, onAttach }) {
  const sorted = [...revisions].sort((a, b) => b.revisionNumber - a.revisionNumber);
  return (
    <div className="mt-2 pt-2 border-t border-[var(--leon-line)] space-y-1.5">
      {sorted.map(r => (
        <div key={r.id} className="text-[11px] text-[var(--leon-black)]/50">
          <p className="flex items-center gap-1.5 flex-wrap">
            <Badge tone="neutral">Rev {r.revisionNumber}</Badge> {fmtMoney(r.revisedAmount)}{r.previousAmount != null && r.previousAmount !== r.revisedAmount ? ` (was ${fmtMoney(r.previousAmount)})` : ''} · {fmtDate(r.date)}{r.revisedBy ? ` · ${r.revisedBy}` : ''}
            {(r.file || onAttach) && <FileField name={r.file} url={r.fileUrl} onChange={onAttach ? (fname, url) => onAttach(r.id, fname, url) : () => {}} editable={!!onAttach} />}
          </p>
          {r.reasonForRevision && <p>Reason: {r.reasonForRevision}</p>}
          {r.notes && <p>{r.notes}</p>}
        </div>
      ))}
    </div>
  );
}
// Shared between Vendor Estimates (ProcurementTab) and Freight Estimates
// (ExportTab) — both use the same makeRevisionEntry-shaped revisions[] array.
function EstimateDetailModal({ open, estimate, partyType, onClose, ctx, project }) {
  if (!estimate) return null;
  const scope = estimate.scopeId ? project.scopes.find(s => s.id === estimate.scopeId) : null;
  const po = estimate.poId ? (partyType === 'Freight' ? project.freightPOs : project.purchaseOrders).find(p => p.id === estimate.poId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Estimate ${estimate.estimateNumber || ''} — ${estimate.vendorName}`} printable
      fields={[
        { label: partyType === 'Freight' ? 'Carrier' : 'Vendor', value: estimate.vendorName }, { label: 'Status', value: estimate.status },
        { label: 'Amount', value: fmtMoney(estimate.amount) }, { label: 'Category', value: estimate.category || 'Original Order' },
        { label: 'Scope', value: scope ? scope.name : '—' }, { label: 'Date', value: fmtDate(estimate.date) },
        { label: 'Description', value: estimate.description }, { label: 'Unplanned Cause', value: estimate.unplannedReason },
        { label: 'PM Approved', value: estimate.pmApproved ? `Yes — ${estimate.pmApprovedBy || ''}` : 'No' },
        { label: 'Ownership Approved', value: estimate.ownerApproved ? `Yes — ${estimate.ownerApprovedBy || ''}` : 'No' },
      ]}
      relatedRecords={po ? [{ label: `PO ${po.poNumber}`, onClick: () => ctx.goProjectTab(project.id, partyType === 'Freight' ? 'export' : 'procurement') }] : []}
      history={(estimate.revisions || []).map(r => ({ ...r, user: r.revisedBy, reason: r.reasonForRevision }))}
    />
  );
}
function PoCard({ ctx, project, po, partyType, editable }) {
  const [revModal, setRevModal] = useState(false);
  const [convertModal, setConvertModal] = useState(false);
  const [detailModal, setDetailModal] = useState(false);
  const [editModal, setEditModal] = useState(false);
  const scope = project.scopes.find(s => s.id === po.scopeId);
  const canConvert = editable && !['Converted to PI', 'Cancelled'].includes(po.status);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="flex items-center gap-2 flex-wrap cursor-pointer" onClick={() => setDetailModal(true)}>
          <Badge tone="black">{po.poNumber || po.id}</Badge>
          {po.category && <Badge tone="neutral">{po.category}</Badge>}
          {scope ? <Badge tone="neutral">{scope.name}</Badge> : <Badge tone="red">No Scope</Badge>}
          <StatusBadge status={po.status} />
        </div>
        <div className="flex items-center gap-1.5">
          {ctx.canEditPOPI && partyType !== 'Freight' && <Button size="sm" variant="ghost" onClick={() => setEditModal(true)}>✎ Edit</Button>}
          {editable && <Button size="sm" variant="ghost" onClick={() => setRevModal(true)}>+ Add Revision</Button>}
        </div>
      </div>
      <EditPurchaseOrderModal open={editModal} onClose={() => setEditModal(false)} ctx={ctx} project={project} po={po} />
      <RecordDetailModal open={detailModal} onClose={() => setDetailModal(false)} title={`Purchase Order — ${po.poNumber || po.id}`}
        fields={[
          { label: partyType === 'Freight' ? 'Carrier' : 'Vendor', value: po.vendorName || po.carrier },
          { label: 'Status', value: po.status }, { label: 'Amount', value: `${fmtMoney(po.amount)}${po.currency && po.currency !== 'USD' ? ' ' + po.currency : ''}` },
          { label: 'Scope', value: scope ? scope.name : '—' }, { label: 'Issued', value: fmtDate(po.issuedDate) },
          { label: 'Required Date', value: po.requiredDate ? fmtDate(po.requiredDate) : '—' }, { label: 'Delivery Terms', value: po.deliveryTerms },
          { label: 'Notes', value: po.notes },
        ]}
        attachments={po.piFileUrl ? [{ name: po.piFile, url: po.piFileUrl }] : []}
        history={po.revisions || []}
      />
      <p className="text-sm font-bold mt-1">{po.vendorName || po.carrier} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(po.amount)} {po.currency && po.currency !== 'USD' ? po.currency : ''} · issued {fmtDate(po.issuedDate)}{po.requiredDate ? ` · required by ${fmtDate(po.requiredDate)}` : ''}</span></p>
      {(po.deliveryTerms || po.notes) && <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{po.deliveryTerms && <>Delivery: {po.deliveryTerms}</>}{po.deliveryTerms && po.notes ? ' · ' : ''}{po.notes}</p>}
      <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">PI/PO document: <FileField name={po.piFile} url={po.piFileUrl} onChange={(fname, url) => (partyType === 'Freight' ? ctx.updateFreightPO : ctx.updatePurchaseOrder)(project.id, po.id, { piFile: fname, piFileUrl: url })} editable={editable} placeholder="None uploaded" /></p>
      {po.paymentTerms && po.paymentTerms.length > 0 && (
        <div className="flex gap-2 mt-2 flex-wrap">
          {po.paymentTerms.map(t => (
            <div key={t.id} className="flex items-center gap-2 border border-[var(--leon-line)] rounded-md px-2 py-1">
              <span className="text-xs font-semibold">{t.label} ({t.pct}%)</span>
              {editable ? (
                <Select value={t.status} onChange={e => ctx.setPOTermStatus(project.id, po.id, t.id, e.target.value)} className="!py-0.5 !text-xs !w-28">
                  {['Not Due', 'Due', 'Paid'].map(s => <option key={s}>{s}</option>)}
                </Select>
              ) : <StatusBadge status={t.status} />}
            </div>
          ))}
        </div>
      )}
      {canConvert && <div className="mt-2"><Button size="sm" variant="outline" onClick={() => setConvertModal(true)}>Convert to Proforma Invoice</Button></div>}
      {po.revisions && po.revisions.length > 0 && (
        <RevisionHistoryList revisions={po.revisions} editable={false} />
      )}
      <AddPoRevisionModal open={revModal} onClose={() => setRevModal(false)} ctx={ctx} project={project} po={po} partyType={partyType} />
      <ConvertToPiModal open={convertModal} onClose={() => setConvertModal(false)} ctx={ctx} project={project} po={po} partyType={partyType} />
    </div>
  );
}
function AddPoRevisionModal({ open, onClose, ctx, project, po, partyType }) {
  const blank = { date: todayISO(), amount: '', reasonForRevision: '', notes: '', file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, amount: po ? po.amount : '' }); }, [open, po]);
  if (!po) return null;
  function submit() {
    if (!form.amount) return;
    (partyType === 'Freight' ? ctx.addFreightPORevision : ctx.addPurchaseOrderRevision)(project.id, po.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Revision — ${po.poNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Revision</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Revised Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
        </div>
        <Field label="Reason for Revision"><TextInput value={form.reasonForRevision} onChange={e => setForm({ ...form, reasonForRevision: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
      </div>
    </Modal>
  );
}
// Admin/Accounting only (ctx.canEditPOPI) — corrects the PO's own header
// fields, which otherwise have no edit path at all. Amount stays out of
// this form on purpose; it only ever changes through Add Revision, to keep
// that audit trail intact.
function EditPurchaseOrderModal({ open, onClose, ctx, project, po }) {
  const blank = { vendorName: '', category: '', scopeId: '', issuedDate: '', requiredDate: '', deliveryTerms: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [error, setError] = useState('');
  useEffect(() => { if (open && po) { setForm({ vendorName: po.vendorName || '', category: po.category || '', scopeId: po.scopeId || '', issuedDate: po.issuedDate || '', requiredDate: po.requiredDate || '', deliveryTerms: po.deliveryTerms || '', notes: po.notes || '' }); setError(''); } }, [open, po]);
  if (!po) return null;
  function submit() {
    if (!form.scopeId) { setError('Every PO must be assigned to a scope.'); return; }
    ctx.updatePurchaseOrder(project.id, po.id, { ...form, requiredDate: form.requiredDate || null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Edit — ${po.poNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Vendor Name"><TextInput value={form.vendorName} onChange={e => setForm({ ...form, vendorName: e.target.value })} /></Field>
          <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>{VENDOR_ESTIMATE_CATEGORIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
        </div>
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">— select a scope —</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
        <div className="grid grid-cols-2 gap-3">
          <Field label="Issued Date"><TextInput type="date" value={form.issuedDate} onChange={e => setForm({ ...form, issuedDate: e.target.value })} /></Field>
          <Field label="Required Date"><TextInput type="date" value={form.requiredDate} onChange={e => setForm({ ...form, requiredDate: e.target.value })} /></Field>
        </div>
        <Field label="Delivery Terms"><TextInput value={form.deliveryTerms} onChange={e => setForm({ ...form, deliveryTerms: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function ConvertToPiModal({ open, onClose, ctx, project, po, partyType }) {
  const blank = { piDate: todayISO(), currency: 'USD', amount: '', paymentTerms: '', depositRequirement: '', balanceRequirement: '', freight: '', taxes: '', duties: '', otherCharges: '', file: '', fileUrl: null, notes: '', materialLines: [] };
  const [form, setForm] = useState(blank);
  const [importError, setImportError] = useState('');
  useEffect(() => { if (open) { setForm({ ...blank, amount: po ? po.amount : '', currency: po ? po.currency : 'USD' }); setImportError(''); } }, [open, po]);
  if (!po) return null;
  const variance = form.amount && po.amount ? ((Number(form.amount) - po.amount) / po.amount) * 100 : 0;
  function submit() {
    if (!form.amount) return;
    ctx.convertPOToPI(project.id, partyType, po.id, { ...form, amount: Number(form.amount), freight: Number(form.freight) || 0, taxes: Number(form.taxes) || 0, duties: Number(form.duties) || 0, otherCharges: Number(form.otherCharges) || 0 });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Convert ${po.poNumber} to Proforma Invoice`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create Proforma Invoice</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Vendor, project, scope, and currency carry forward from the PO automatically — enter the amount the vendor actually issued the PI for, which may differ from the PO.</p>
        <div className="grid grid-cols-3 gap-3">
          <Field label="PI Date"><TextInput type="date" value={form.piDate} onChange={e => setForm({ ...form, piDate: e.target.value })} /></Field>
          <Field label="Currency"><Select value={form.currency} onChange={e => setForm({ ...form, currency: e.target.value })}>{CURRENCIES.map(c => <option key={c}>{c}</option>)}</Select></Field>
          <Field label="PI Total"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
        </div>
        {form.amount && Math.abs(variance) >= 1 && (
          <p className={`text-xs font-semibold ${variance > 0 ? 'text-[var(--leon-red)]' : 'text-[var(--leon-green)]'}`}>
            {variance > 0 ? '+' : ''}{variance.toFixed(1)}% vs PO ({fmtMoney(po.amount)})
          </p>
        )}
        <div className="grid grid-cols-2 gap-3">
          <Field label="Payment Terms"><TextInput value={form.paymentTerms} onChange={e => setForm({ ...form, paymentTerms: e.target.value })} /></Field>
          <Field label="Deposit Requirement"><TextInput value={form.depositRequirement} onChange={e => setForm({ ...form, depositRequirement: e.target.value })} /></Field>
        </div>
        <Field label="Balance Requirement"><TextInput value={form.balanceRequirement} onChange={e => setForm({ ...form, balanceRequirement: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Freight"><TextInput type="number" value={form.freight} onChange={e => setForm({ ...form, freight: e.target.value })} /></Field>
          <Field label="Taxes"><TextInput type="number" value={form.taxes} onChange={e => setForm({ ...form, taxes: e.target.value })} /></Field>
          <Field label="Duties"><TextInput type="number" value={form.duties} onChange={e => setForm({ ...form, duties: e.target.value })} /></Field>
        </div>
        <Field label="Other Charges"><TextInput type="number" value={form.otherCharges} onChange={e => setForm({ ...form, otherCharges: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <PiMaterialListEditor ctx={ctx} lines={form.materialLines} onChange={lines => setForm({ ...form, materialLines: lines })} importError={importError} setImportError={setImportError} />
      </div>
    </Modal>
  );
}
// Shared material-list editor for a PI — manual line entry (optionally
// linked to the Material Library) plus a real .xlsx import (§ PI material
// list request). Reused by both the initial PO→PI conversion and editing an
// existing PI's list later (ctx.updatePiMaterialLines).
function PiMaterialListModal({ open, onClose, ctx, project, pi }) {
  const [lines, setLines] = useState([]);
  const [importError, setImportError] = useState('');
  useEffect(() => { if (open) { setLines(pi.materialLines); setImportError(''); } }, [open, pi]);
  function submit() { ctx.updatePiMaterialLines(project.id, pi.id, lines); onClose(); }
  return (
    <Modal open={open} onClose={onClose} wide title={`Material List — ${pi.piNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <PiMaterialListEditor ctx={ctx} lines={lines} onChange={setLines} importError={importError} setImportError={setImportError} />
    </Modal>
  );
}
function PiMaterialListEditor({ ctx, lines, onChange, importError, setImportError }) {
  const fileRef = useRef(null);
  // Multi-select for bulk removal. An Excel import routinely brings in rows
  // that don't belong on this PI (other packages, subtotal rows, options not
  // taken) — clearing them one ✕ at a time is unworkable at import scale.
  const [selected, setSelected] = useState(() => new Set());
  const [lastClicked, setLastClicked] = useState(null);
  // Drop ids that no longer exist, so a stale selection can never delete a
  // line the user can't see (e.g. after a re-import replaces the list).
  const liveSelected = useMemo(() => {
    const ids = new Set(lines.map(l => l.id));
    return new Set([...selected].filter(id => ids.has(id)));
  }, [selected, lines]);
  const allSelected = lines.length > 0 && liveSelected.size === lines.length;

  function toggleLine(id, shiftKey) {
    setSelected(prev => {
      const next = new Set(prev);
      // Shift-click selects the run between the last click and this one —
      // the fast path for trimming a contiguous block off an import.
      if (shiftKey && lastClicked) {
        const from = lines.findIndex(l => l.id === lastClicked);
        const to = lines.findIndex(l => l.id === id);
        if (from > -1 && to > -1) {
          const [a, b] = from < to ? [from, to] : [to, from];
          const turningOn = !next.has(id);
          for (let i = a; i <= b; i++) { if (turningOn) next.add(lines[i].id); else next.delete(lines[i].id); }
          return next;
        }
      }
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
    setLastClicked(id);
  }
  function toggleAll() {
    setSelected(allSelected ? new Set() : new Set(lines.map(l => l.id)));
    setLastClicked(null);
  }
  function deleteSelected() {
    if (!liveSelected.size) return;
    if (!confirm(`Remove ${liveSelected.size} material line${liveSelected.size === 1 ? '' : 's'}?`)) return;
    onChange(lines.filter(l => !liveSelected.has(l.id)));
    setSelected(new Set());
    setLastClicked(null);
  }

  function addLine() { onChange([...lines, makePiMaterialLine({})]); }
  function updateLine(id, fields) { onChange(lines.map(l => l.id === id ? { ...l, ...fields } : l)); }
  function removeLine(id) { onChange(lines.filter(l => l.id !== id)); }
  async function onImportFile(e) {
    const file = e.target.files[0];
    e.target.value = '';
    if (!file) return;
    setImportError('');
    try {
      const rows = await parseExcelMaterialList(file);
      if (!rows.length) { setImportError('No rows found in that file.'); return; }
      const imported = rows.map(r => {
        const match = ctx.materialLibrary.find(m => m.active && m.name.trim().toLowerCase() === r.description.trim().toLowerCase());
        return makePiMaterialLine({ ...r, materialId: match ? match.id : null });
      });
      onChange([...lines, ...imported]);
    } catch (err) {
      setImportError(err.message || 'Could not import that file.');
    }
  }
  return (
    <div className="pt-2 border-t border-[var(--leon-line)]">
      <div className="flex items-center justify-between mb-1.5">
        <div className="flex items-center gap-3">
          <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Material List</p>
          {lines.length > 0 && (
            <label className="flex items-center gap-1.5 text-xs text-[var(--leon-black)]/60 cursor-pointer" title="Select all lines">
              <input type="checkbox" checked={allSelected} onChange={toggleAll} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
              Select all
            </label>
          )}
          {liveSelected.size > 0 && (
            <>
              <span className="text-xs font-semibold text-[var(--leon-brown)]">{liveSelected.size} selected</span>
              <button onClick={deleteSelected} className="text-xs font-semibold text-[var(--leon-red)] hover:underline">Delete selected</button>
              <button onClick={() => { setSelected(new Set()); setLastClicked(null); }} className="text-xs text-[var(--leon-black)]/50 hover:underline">Clear</button>
            </>
          )}
        </div>
        <div className="flex items-center gap-3">
          <button onClick={() => fileRef.current.click()} className="text-xs text-[var(--leon-brown)] font-semibold">⬆ Import from Excel</button>
          <input ref={fileRef} type="file" accept=".xlsx,.xls" className="hidden" onChange={onImportFile} />
        </div>
      </div>
      {importError && <p className="text-xs text-[var(--leon-red)] mb-1.5">{importError}</p>}
      {lines.length === 0 ? <EmptyState text="No material lines yet — add manually or import from Excel." /> : (
        <div className="space-y-2 mb-2">
          {lines.map(l => (
            <div key={l.id} className={`border rounded-lg p-1.5 space-y-1 ${liveSelected.has(l.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)]'}`}>
              <div className="grid grid-cols-12 gap-1.5 items-center">
                <div className="col-span-1 flex justify-center">
                  <input type="checkbox" checked={liveSelected.has(l.id)}
                    onChange={e => toggleLine(l.id, e.nativeEvent.shiftKey)}
                    onClick={e => e.stopPropagation()}
                    title="Select this line (shift-click to select a range)"
                    className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
                </div>
                <div className="col-span-10">
                  <Select value={l.materialId || ''} onChange={e => {
                    const m = ctx.materialLibrary.find(x => x.id === e.target.value);
                    updateLine(l.id, { materialId: e.target.value || null, description: m ? m.name : l.description });
                  }} className="!py-1 !text-xs">
                    <option value="">— free text —</option>
                    {ctx.materialLibrary.filter(m => m.active).map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                  </Select>
                  {!l.materialId && <TextInput value={l.description} onChange={e => updateLine(l.id, { description: e.target.value })} placeholder="Description" className="!py-1 !text-xs !mt-1" />}
                </div>
                <IconBtn title="Remove" onClick={() => removeLine(l.id)} className="col-span-1">✕</IconBtn>
              </div>
              <div className="grid grid-cols-12 gap-1.5 items-center">
                <TextInput value={l.itemNo || ''} onChange={e => updateLine(l.id, { itemNo: e.target.value })} placeholder="Item #" title="Line/item number on the source document" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.itemCode} onChange={e => updateLine(l.id, { itemCode: e.target.value })} placeholder="Item Code" title="Manufacturer's item / SKU code" className="col-span-3 !py-1 !text-xs" />
                <TextInput value={l.itemName || ''} onChange={e => updateLine(l.id, { itemName: e.target.value })} placeholder="Item Name" className="col-span-3 !py-1 !text-xs" />
                <TextInput type="number" value={l.quantity} onChange={e => updateLine(l.id, { quantity: Number(e.target.value) || 0 })} placeholder="Qty" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.unit} onChange={e => updateLine(l.id, { unit: e.target.value })} placeholder="Unit" className="col-span-2 !py-1 !text-xs" />
              </div>
              <div className="grid grid-cols-12 gap-1.5 items-center">
                <TextInput type="number" value={l.unitCost} onChange={e => updateLine(l.id, { unitCost: Number(e.target.value) || 0 })} placeholder="Unit Cost" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.width || ''} onChange={e => updateLine(l.id, { width: e.target.value })} placeholder="W" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.height || ''} onChange={e => updateLine(l.id, { height: e.target.value })} placeholder="H" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.depth || ''} onChange={e => updateLine(l.id, { depth: e.target.value })} placeholder="D" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.thickness || ''} onChange={e => updateLine(l.id, { thickness: e.target.value })} placeholder="Thk" className="col-span-2 !py-1 !text-xs" />
                <TextInput value={l.dimensions} onChange={e => updateLine(l.id, { dimensions: e.target.value })} placeholder="Dims" title="Combined dimensions, as shown on documents" className="col-span-2 !py-1 !text-xs" />
              </div>
            </div>
          ))}
        </div>
      )}
      <button onClick={addLine} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Line</button>
    </div>
  );
}
function PiCard({ ctx, project, pi, editable }) {
  const [revModal, setRevModal] = useState(false);
  const [detailModal, setDetailModal] = useState(false);
  const [materialsModal, setMaterialsModal] = useState(false);
  const [editModal, setEditModal] = useState(false);
  const scope = project.scopes.find(s => s.id === pi.scopeId);
  const { po, overThreshold, diffAmount, diffPct } = ctx.piExceedsApprovedPO(project, pi);
  const canApprovePayment = canApproveInvoices(ctx.currentRole);
  const blocked = overThreshold && !canApprovePayment;
  const linkedInvoice = pi.apInvoiceId ? project.apInvoices.find(i => i.id === pi.apInvoiceId) : null;
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="flex items-center gap-2 flex-wrap cursor-pointer" onClick={() => setDetailModal(true)}>
          <Badge tone="black">{pi.piNumber}</Badge>
          {po && <Badge tone="neutral">from {po.poNumber}</Badge>}
          {scope ? <Badge tone="neutral">{scope.name}</Badge> : <Badge tone="red">No Scope</Badge>}
          <StatusBadge status={pi.status} />
        </div>
        <div className="flex items-center gap-1.5">
          {ctx.canEditPOPI && <Button size="sm" variant="ghost" onClick={() => setEditModal(true)}>✎ Edit</Button>}
          {editable && <Button size="sm" variant="ghost" onClick={() => setRevModal(true)}>+ Add Revision</Button>}
        </div>
      </div>
      <EditProformaInvoiceModal open={editModal} onClose={() => setEditModal(false)} ctx={ctx} project={project} pi={pi} />
      <RecordDetailModal open={detailModal} onClose={() => setDetailModal(false)} title={`Proforma Invoice — ${pi.piNumber}`}
        fields={[
          { label: 'Vendor', value: pi.vendorName }, { label: 'Status', value: pi.status },
          { label: 'Amount', value: `${fmtMoney(pi.amount)}${pi.currency !== 'USD' ? ' ' + pi.currency : ''}` },
          { label: 'Related PO', value: po ? po.poNumber : '—' }, { label: 'Scope', value: scope ? scope.name : '—' },
          { label: 'PI Date', value: fmtDate(pi.piDate) }, { label: 'Payment Terms', value: pi.paymentTerms },
          { label: 'Freight', value: pi.freight ? fmtMoney(pi.freight) : '—' }, { label: 'Taxes', value: pi.taxes ? fmtMoney(pi.taxes) : '—' },
          { label: 'Duties', value: pi.duties ? fmtMoney(pi.duties) : '—' }, { label: 'Notes', value: pi.notes },
        ]}
        attachments={pi.fileUrl ? [{ name: pi.file, url: pi.fileUrl }] : []}
        history={pi.revisions || []}
      />
      <p className="text-sm font-bold mt-1">{pi.vendorName} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(pi.amount)} {pi.currency !== 'USD' ? pi.currency : ''} · dated {fmtDate(pi.piDate)}</span></p>
      {po && (
        <div className="grid sm:grid-cols-4 gap-2 mt-1.5 text-xs">
          <span>PO Amount: <strong>{fmtMoney(po.amount)}</strong></span>
          <span>PI Amount: <strong>{fmtMoney(pi.amount)}</strong></span>
          <span className={diffAmount > 0 ? 'text-[var(--leon-red)]' : diffAmount < 0 ? 'text-[var(--leon-green)]' : ''}>Difference: <strong>{fmtMoney(diffAmount)}</strong></span>
          <span className={diffAmount > 0 ? 'text-[var(--leon-red)]' : diffAmount < 0 ? 'text-[var(--leon-green)]' : ''}>Difference %: <strong>{diffPct > 0 ? '+' : ''}{diffPct.toFixed(1)}%</strong></span>
        </div>
      )}
      {overThreshold && (
        <div className="mt-2 bg-[#fbe7e7] border border-[var(--leon-red)] rounded-lg p-2">
          <p className="text-xs font-bold text-[var(--leon-red)]">⚠ PI EXCEEDS APPROVED PURCHASE ORDER — REVIEW REQUIRED</p>
          <p className="text-[11px] text-[var(--leon-red)]">More than {(PI_VARIANCE_REVIEW_THRESHOLD * 100).toFixed(0)}% over the approved PO — requires Admin or General Manager review before payment.</p>
        </div>
      )}
      {(pi.paymentTerms || pi.depositRequirement || pi.balanceRequirement) && (
        <p className="text-xs text-[var(--leon-black)]/50 mt-1.5">{pi.paymentTerms && <>Terms: {pi.paymentTerms} · </>}{pi.depositRequirement && <>Deposit: {pi.depositRequirement} · </>}{pi.balanceRequirement && <>Balance: {pi.balanceRequirement}</>}</p>
      )}
      {(pi.freight || pi.taxes || pi.duties || pi.otherCharges) ? (
        <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{pi.freight ? `Freight: ${fmtMoney(pi.freight)} · ` : ''}{pi.taxes ? `Taxes: ${fmtMoney(pi.taxes)} · ` : ''}{pi.duties ? `Duties: ${fmtMoney(pi.duties)} · ` : ''}{pi.otherCharges ? `Other: ${fmtMoney(pi.otherCharges)}` : ''}</p>
      ) : null}
      <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Document: <FileField name={pi.file} url={pi.fileUrl} editable={false} onChange={() => {}} /></p>
      {linkedInvoice && <p className="text-xs mt-1"><Badge tone="green">✓ AP Invoice {linkedInvoice.invoiceNumber} — {linkedInvoice.paymentStatus}</Badge></p>}
      <p className="text-xs text-[var(--leon-black)]/50 mt-1">
        Material List: <strong>{pi.materialLines.length} line{pi.materialLines.length === 1 ? '' : 's'}</strong>
        {editable && <button onClick={() => setMaterialsModal(true)} className="ml-2 text-[var(--leon-brown)] font-semibold">✎ Edit</button>}
      </p>
      <PiMaterialListModal open={materialsModal} onClose={() => setMaterialsModal(false)} ctx={ctx} project={project} pi={pi} />

      {editable && (
        <div className="flex items-center gap-2 mt-2 flex-wrap">
          {!['Approved for Payment', 'Partially Paid', 'Paid', 'Cancelled', 'Superseded'].includes(pi.status) && PI_STATUSES.filter(s => !['Approved for Payment', 'Partially Paid', 'Paid'].includes(s)).map(s => (
            s !== pi.status && <Button key={s} size="sm" variant="ghost" onClick={() => ctx.setPIStatus(project.id, pi.id, s)}>{s}</Button>
          ))}
          {!pi.apInvoiceId && pi.status !== 'Cancelled' && (
            blocked
              ? <span className="text-[11px] text-[var(--leon-red)] italic">Blocked — requires Admin/GM review (exceeds PO by {diffPct.toFixed(1)}%)</span>
              : <Button size="sm" variant="danger" onClick={() => ctx.approvePIForPayment(project.id, pi.id)}>Approve for Payment</Button>
          )}
        </div>
      )}
      {pi.notes && <p className="text-xs text-[var(--leon-black)]/50 mt-1">{pi.notes}</p>}
      {pi.revisions.length > 0 && <RevisionHistoryList revisions={pi.revisions} editable={editable} />}
      <AddPiRevisionModal open={revModal} onClose={() => setRevModal(false)} ctx={ctx} project={project} pi={pi} />
    </div>
  );
}
function AddPiRevisionModal({ open, onClose, ctx, project, pi }) {
  const blank = { date: todayISO(), amount: '', reasonForRevision: '', notes: '', file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, amount: pi ? pi.amount : '' }); }, [open, pi]);
  if (!pi) return null;
  function submit() {
    if (!form.amount) return;
    ctx.addProformaInvoiceRevision(project.id, pi.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Revision — ${pi.piNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Revision</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Revised Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
        </div>
        <Field label="Reason for Revision"><TextInput value={form.reasonForRevision} onChange={e => setForm({ ...form, reasonForRevision: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
      </div>
    </Modal>
  );
}
// Admin/Accounting only (ctx.canEditPOPI) — same intent as
// EditPurchaseOrderModal: corrects header fields with no other edit path.
// Amount is deliberately excluded — it only ever changes through Add
// Revision, preserving that audit trail.
function EditProformaInvoiceModal({ open, onClose, ctx, project, pi }) {
  const blank = { vendorName: '', scopeId: '', piDate: '', paymentTerms: '', depositRequirement: '', balanceRequirement: '', freight: '', taxes: '', duties: '', otherCharges: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [error, setError] = useState('');
  useEffect(() => { if (open && pi) { setForm({ vendorName: pi.vendorName || '', scopeId: pi.scopeId || '', piDate: pi.piDate || '', paymentTerms: pi.paymentTerms || '', depositRequirement: pi.depositRequirement || '', balanceRequirement: pi.balanceRequirement || '', freight: pi.freight || '', taxes: pi.taxes || '', duties: pi.duties || '', otherCharges: pi.otherCharges || '', notes: pi.notes || '' }); setError(''); } }, [open, pi]);
  if (!pi) return null;
  function submit() {
    if (!form.scopeId) { setError('Every PI must be assigned to a scope.'); return; }
    ctx.updateProformaInvoice(project.id, pi.id, { ...form, freight: Number(form.freight) || 0, taxes: Number(form.taxes) || 0, duties: Number(form.duties) || 0, otherCharges: Number(form.otherCharges) || 0 });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Edit — ${pi.piNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Vendor Name"><TextInput value={form.vendorName} onChange={e => setForm({ ...form, vendorName: e.target.value })} /></Field>
          <Field label="PI Date"><TextInput type="date" value={form.piDate} onChange={e => setForm({ ...form, piDate: e.target.value })} /></Field>
        </div>
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">— select a scope —</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
        <div className="grid grid-cols-2 gap-3">
          <Field label="Payment Terms"><TextInput value={form.paymentTerms} onChange={e => setForm({ ...form, paymentTerms: e.target.value })} /></Field>
          <Field label="Notes"><TextInput value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Deposit Requirement"><TextInput value={form.depositRequirement} onChange={e => setForm({ ...form, depositRequirement: e.target.value })} /></Field>
          <Field label="Balance Requirement"><TextInput value={form.balanceRequirement} onChange={e => setForm({ ...form, balanceRequirement: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Freight"><TextInput type="number" value={form.freight} onChange={e => setForm({ ...form, freight: e.target.value })} /></Field>
          <Field label="Taxes"><TextInput type="number" value={form.taxes} onChange={e => setForm({ ...form, taxes: e.target.value })} /></Field>
          <Field label="Duties"><TextInput type="number" value={form.duties} onChange={e => setForm({ ...form, duties: e.target.value })} /></Field>
        </div>
        <Field label="Other Charges"><TextInput type="number" value={form.otherCharges} onChange={e => setForm({ ...form, otherCharges: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddVendorEstimateModal({ open, onClose, ctx, project }) {
  const blank = { vendorId: '', scopeId: '', category: VENDOR_ESTIMATE_CATEGORIES[0], description: '', amount: '', date: todayISO(), file: '', fileUrl: null, unplannedReason: UNPLANNED_COST_REASONS[0], recoverability: RECOVERABILITY_STATUSES[0] };
  const [form, setForm] = useState(blank);
  const [error, setError] = useState('');
  useEffect(() => { if (open) { setForm({ ...blank, vendorId: ctx.vendors[0]?.id || '' }); setError(''); } }, [open]);
  const unplanned = isUnplannedCost(form.category);
  // A Vendor Estimate is the only origin point for a PO's scope (POs/PIs
  // both inherit scopeId from here, never set it independently) — every PO
  // and PI needs a real scope, so this is where that's enforced.
  function submit() {
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    if (!vendor || !form.amount) return;
    if (!form.scopeId) { setError('Select a scope — every Vendor Estimate (and the PO/PI it becomes) must be assigned to one.'); return; }
    ctx.addVendorEstimate(project.id, { vendorId: vendor.id, vendorName: vendor.name, scopeId: form.scopeId, category: form.category, description: form.description, amount: Number(form.amount), date: form.date, file: form.file, fileUrl: form.fileUrl, unplannedReason: unplanned ? form.unplannedReason : null, recoverability: unplanned ? form.recoverability : null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Add Vendor Estimate" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Estimate</Button></>}>
      <div className="space-y-3">
        <Field label="Vendor" hint="Managed under the Vendors directory.">
          <Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}>
            {ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
          </Select>
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>{VENDOR_ESTIMATE_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}</Select></Field>
          <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">— select a scope —</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
        {error && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {error}</p>}
        <Field label="Description"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Quotation (optional)"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        {unplanned && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 space-y-3 bg-[var(--leon-cream)]">
            <p className="text-xs font-bold text-[var(--leon-red)]">⚠ Unplanned Business Cost / Potential Loss</p>
            <p className="text-xs text-[var(--leon-black)]/60">This category wasn't part of the original approved estimate, an approved Change Order, or Samples. Why was this cost not included in the original project budget?</p>
            <Field label="Reason"><Select value={form.unplannedReason} onChange={e => setForm({ ...form, unplannedReason: e.target.value })}>{UNPLANNED_COST_REASONS.map(r => <option key={r}>{r}</option>)}</Select></Field>
            <Field label="Recoverability"><Select value={form.recoverability} onChange={e => setForm({ ...form, recoverability: e.target.value })}>{RECOVERABILITY_STATUSES.map(r => <option key={r}>{r}</option>)}</Select></Field>
          </div>
        )}
      </div>
    </Modal>
  );
}
function AddVendorEstimateRevisionModal({ open, ve, onClose, ctx, project }) {
  const [form, setForm] = useState({ amount: '', date: todayISO(), file: '', fileUrl: null, note: '' });
  useEffect(() => { if (open) setForm({ amount: '', date: todayISO(), file: '', fileUrl: null, note: '' }); }, [open]);
  if (!ve) return null;
  function submit() {
    if (!form.amount) return;
    ctx.addVendorEstimateRevision(project.id, ve.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Revision — ${ve.vendorName}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Revision</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Quotation File"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Production (Project -> Scope -> Vendor -> Production Record) ---------
// The Production Hub reads as four questions, in the order they get asked:
// where does the job stand, are the drawings right, how far along is the shop,
// and did it pass QC. Quality Control lives here rather than as its own project
// tab because it IS part of production — a scope is not finished until it has
// passed, which is what the Overview's "Ready to Ship" state says.
const PRODUCTION_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'drawings', label: 'Production Drawings', icon: '📐' },
  { key: 'progress', label: 'Production Progress', icon: '📸' },
  { key: 'qc', label: 'Quality Control', icon: '🔍' },
];
function ProductionTab({ ctx, project }) {
  const editable = ctx.canEdit('production');
  const [sub, setSub] = useState('overview');
  const [vendorFilter, setVendorFilter] = useState('all');
  const [showAddRecord, setShowAddRecord] = useState(false);

  const vendorsUsed = useMemo(() => {
    const ids = [...new Set(project.productionRecords.map(r => r.vendorId).filter(Boolean))];
    return ids.map(id => ctx.vendors.find(v => v.id === id)).filter(Boolean);
  }, [project.productionRecords, ctx.vendors]);

  const scopes = ctx.deptScopes(project);
  const recordsFor = scopeId => project.productionRecords
    .filter(r => r.scopeId === scopeId && (vendorFilter === 'all' || r.vendorId === vendorFilter));

  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {PRODUCTION_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>{t.label}
          </button>
        ))}
      </div>
      <HubTools />

      {sub !== 'qc' && (
        <div className="flex items-center justify-between gap-2 flex-wrap mb-3">
          <Select value={vendorFilter} onChange={e => setVendorFilter(e.target.value)} className="!w-auto">
            <option value="all">All Vendors</option>
            {vendorsUsed.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
          </Select>
          {editable && <Button size="sm" onClick={() => setShowAddRecord(true)}>+ Add Production Record</Button>}
        </div>
      )}

      {sub === 'overview' && <ProductionOverview ctx={ctx} project={project} scopes={scopes} recordsFor={recordsFor} />}
      {sub === 'drawings' && <ProductionDrawingsSub ctx={ctx} project={project} scopes={scopes} recordsFor={recordsFor} editable={editable} />}
      {sub === 'progress' && <ProductionProgressSub ctx={ctx} project={project} scopes={scopes} recordsFor={recordsFor} editable={editable} />}
      {sub === 'qc' && <QualityControlTab ctx={ctx} project={project} />}

      <AddProductionRecordModal open={showAddRecord} onClose={() => setShowAddRecord(false)} ctx={ctx} project={project} />
    </div>
  );
}

// Where every scope stands, and the one state the rest of the job is waiting
// on: ready to ship. It is not a flag anyone sets — it is every production
// record Complete and cleared by QC (scopeReadyForExport), so it cannot say
// "ready" while an inspection is still open.
function ProductionOverview({ ctx, project, scopes, recordsFor }) {
  const alerts = useMemo(
    () => containerBookingAlerts(project, ctx.exportContainers),
    [project, ctx.exportContainers]);
  const ready = scopes.filter(s => scopeReadyForExport(project, s.id));
  return (
    <div>
      {alerts.length > 0 && <ContainerBookingNotice ctx={ctx} project={project} alerts={alerts} />}

      <div className="grid sm:grid-cols-3 gap-3 mb-4">
        <StatBox label="Scopes in production" value={scopes.filter(s => recordsFor(s.id).length).length} />
        <StatBox label="Ready to ship" value={ready.length} />
        <StatBox label="Awaiting a container" value={alerts.length} />
      </div>

      {scopes.length === 0 ? <EmptyState text="No scopes on this project yet." /> : (
        <div className="space-y-2">
          {scopes.map(scope => {
            const records = recordsFor(scope.id);
            const readyToShip = scopeReadyForExport(project, scope.id);
            const alert = alerts.find(a => a.scopeId === scope.id);
            const due = scopeProductionDue(scope);
            const done = records.filter(r => r.status === 'Complete').length;
            const qcOpen = (project.qcInspections || []).filter(i => i.scopeId === scope.id && qcInspectionOpen(i)).length;
            return (
              <div key={scope.id} className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
                <div className="px-3 py-2 flex items-center gap-2 flex-wrap">
                  <span className="text-sm font-bold">{scope.name}</span>
                  <Badge tone="neutral">{scope.familyName}</Badge>
                  <div className="flex-1" />
                  {readyToShip
                    ? <Badge tone="green">✓ Ready to Ship</Badge>
                    : records.length === 0
                      ? <Badge tone="neutral">Not started</Badge>
                      : <Badge tone="blue">{done}/{records.length} complete</Badge>}
                  {qcOpen > 0 && <Badge tone="yellow">{qcOpen} QC open</Badge>}
                  {alert && <Badge tone={alert.severity === 'critical' ? 'red' : 'yellow'}>No container booked</Badge>}
                </div>
                {records.length > 0 && (
                  <div className="px-3 pb-2 text-xs text-[var(--leon-black)]/55 flex flex-wrap gap-x-4 gap-y-0.5">
                    {records.map(r => {
                      const sum = productionRecordSummary(r);
                      return (
                        <span key={r.id}>
                          <b>{r.vendorName}</b> · {sum.productionStatus} · QC {sum.qcStatus}
                          {sum.shipping.approved ? ' · shipping approved' : ''}
                        </span>
                      );
                    })}
                    {due && <span className="text-[var(--leon-black)]/40">Production due {fmtDate(due)}</span>}
                  </div>
                )}
                {!readyToShip && records.length > 0 && (
                  <p className="px-3 pb-2 text-[11px] text-[var(--leon-black)]/40">
                    Ready to Ship needs every production record Complete <b>and</b> cleared by QC.
                  </p>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// The export desk has to book ten days ahead of the production date. This
// raises it against the schedule the rest of the job already runs on, so it
// cannot disagree with the stage dates, and it escalates rather than expiring.
function ContainerBookingNotice({ ctx, project, alerts }) {
  const critical = alerts.filter(a => a.severity === 'critical');
  const tone = critical.length ? 'red' : 'yellow';
  return (
    <div className={`mb-4 rounded-xl border px-3 py-2.5 ${critical.length
      ? 'border-[var(--leon-red)]/50 bg-[var(--leon-red)]/5'
      : 'border-[var(--leon-yellow)]/60 bg-[var(--leon-yellow)]/10'}`}>
      <p className={`text-sm font-bold ${critical.length ? 'text-[var(--leon-red)]' : 'text-[var(--leon-yellow)]'}`}>
        {critical.length ? '! ' : '⚠ '}
        {alerts.length === 1 ? 'A container needs booking' : `${alerts.length} scopes need a container booked`}
      </p>
      <p className="text-xs text-[var(--leon-black)]/60 mt-0.5">
        Containers are booked <b>{CONTAINER_BOOKING_LEAD_DAYS} days before production finishes</b> —
        by the time the last crate is closed the sailing is gone.
      </p>
      <div className="mt-2 space-y-1">
        {alerts.map(a => (
          <p key={a.scopeId} className="text-xs">
            <b>{a.scopeName}</b> — production due {fmtDate(a.productionDue)};
            should have been booked by {fmtDate(a.bookBy)}
            {a.daysLate > 0 ? ` (${a.daysLate} day${a.daysLate === 1 ? '' : 's'} ago)` : ''}.
            {a.productionComplete && <b className="text-[var(--leon-red)]"> Production is already complete.</b>}
          </p>
        ))}
      </div>
      {/* A container is BOOKED in the Export Hub — that is where the record is
          created and the material lines go onto it. Logistics tracks it once it
          exists, which is a different question and a different screen. */}
      <button onClick={() => ctx.goProjectTab(project.id, 'export')}
        className="mt-2 text-xs font-semibold text-[var(--leon-brown)] hover:underline">
        Open the Export Hub to book →
      </button>
    </div>
  );
}

function ProductionDrawingsSub({ ctx, project, scopes, recordsFor, editable }) {
  const any = scopes.some(s => recordsFor(s.id).length);
  if (!any) return <EmptyState text="No production records yet — add one to file its drawings." />;
  return (
    <div className="space-y-2">
      {scopes.map(scope => {
        const records = recordsFor(scope.id);
        if (!records.length) return null;
        const total = records.reduce((n, r) => n + (r.drawings || []).length, 0);
        return (
          <Collapsible key={scope.id} title={scope.name} count={total} defaultOpen>
            {records.map(rec => (
              <ProductionRecordCard key={rec.id} ctx={ctx} project={project} record={rec} editable={editable} only="drawings" />
            ))}
          </Collapsible>
        );
      })}
    </div>
  );
}

function ProductionProgressSub({ ctx, project, scopes, recordsFor, editable }) {
  const any = scopes.some(s => recordsFor(s.id).length);
  if (!any) return <EmptyState text="No production records yet — add one to track progress." />;
  return (
    <div className="space-y-2">
      {scopes.map(scope => {
        const records = recordsFor(scope.id);
        if (!records.length) return null;
        const photos = records.reduce((n, r) => n + (r.photos || []).length, 0);
        return (
          <Collapsible key={scope.id} title={scope.name} count={photos} defaultOpen
            right={<span className="text-[11px] text-[var(--leon-black)]/45">{photos} photo{photos === 1 ? '' : 's'}</span>}>
            {records.map(rec => (
              <ProductionRecordCard key={rec.id} ctx={ctx} project={project} record={rec} editable={editable} only="progress" />
            ))}
          </Collapsible>
        );
      })}
    </div>
  );
}

function ProductionRecordCard({ ctx, project, record, editable, only }) {
  const [drawingModal, setDrawingModal] = useState(false);
  const [photoModal, setPhotoModal] = useState(false);
  const [qcModal, setQcModal] = useState(false);
  const [detailOpen, setDetailOpen] = useState(false);
  const [editOpen, setEditOpen] = useState(false);
  const shipping = productionShippingStatus(record);
  const scope = findScope(project, record.scopeId);

  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="cursor-pointer" onClick={() => setDetailOpen(true)}>
          <p className="text-sm font-bold hover:underline">{record.vendorName} <span className="font-normal text-[var(--leon-black)]/50">— {scope ? scope.name : ''}</span></p>
          <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Created {fmtDate(record.createdDate)} by {record.createdBy}</p>
        </div>
        <div className="flex items-center gap-2">
          {editable && <button onClick={() => setEditOpen(true)} className="text-xs text-[var(--leon-brown)] font-semibold">✎ Edit</button>}
          {editable ? (
            <Select value={record.status} onChange={e => ctx.setProductionRecordStatus(project.id, record.id, e.target.value)} className="!py-1 !text-xs !w-40">
              {PRODUCTION_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          ) : <StatusBadge status={record.status} />}
          {shipping.approved ? <Badge tone="green">Approved for Shipping</Badge> : <Badge tone="red">Not Approved for Shipping</Badge>}
        </div>
      </div>
      <AddProductionRecordModal open={editOpen} editRecord={record} onClose={() => setEditOpen(false)} ctx={ctx} project={project} />
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">{shipping.reason}</p>

      <div className={`grid gap-3 mt-3 ${only ? "" : "md:grid-cols-3"}`}>
        {(!only || only === "drawings") && (
        <div className="border border-[var(--leon-line)] rounded-md p-2.5">
          <div className="flex items-center justify-between mb-1.5">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Drawings ({record.drawings.length})</p>
            {editable && <Button size="sm" variant="ghost" onClick={() => setDrawingModal(true)}>+ Add Revision</Button>}
          </div>
          {record.drawings.length === 0 ? <EmptyState text="None yet." /> : (
            <div className="space-y-1.5">
              {[...record.drawings].sort((a, b) => b.revisionNumber - a.revisionNumber).map(d => (
                <div key={d.id} className="text-[11px]">
                  <div className="flex items-center gap-1.5 flex-wrap">
                    <Badge tone={d.status === 'Current' ? 'green' : 'neutral'}>Rev {d.revisionNumber} · {d.status}</Badge>
                    <span className="text-[var(--leon-black)]/40">{fmtDate(d.revisionDate)}</span>
                  </div>
                  <div className="mt-0.5"><FileField name={d.file} url={d.fileUrl} editable={false} onChange={() => {}} /></div>
                  {d.notes && <p className="text-[var(--leon-black)]/50 mt-0.5">{d.notes}</p>}
                </div>
              ))}
            </div>
          )}
        </div>
        )}

        {(!only || only === "progress") && (
        <div className="border border-[var(--leon-line)] rounded-md p-2.5">
          <div className="flex items-center justify-between mb-1.5">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Photos ({record.photos.length})</p>
            {editable && <Button size="sm" variant="ghost" onClick={() => setPhotoModal(true)}>+ Add Photos</Button>}
          </div>
          {record.photos.length === 0 ? <EmptyState text="None yet." /> : (
            <div className="space-y-1.5">
              {[...record.photos].sort((a, b) => b.revisionNumber - a.revisionNumber).map(ph => (
                <div key={ph.id} className="text-[11px]">
                  <div className="flex items-center gap-1.5 flex-wrap text-[var(--leon-black)]/40">
                    <span>{fmtDate(ph.date)}</span>
                    {ph.floor && <span>· Floor {ph.floor}</span>}
                    {ph.area && <span>· {ph.area}</span>}
                    {ph.unit && <span>· Unit {ph.unit}</span>}
                  </div>
                  {ph.item && <p className="font-semibold">{ph.item}</p>}
                  <div className="mt-0.5"><FileField name={ph.file} url={ph.fileUrl} editable={false} onChange={() => {}} /></div>
                  {ph.notes && <p className="text-[var(--leon-black)]/50 mt-0.5">{ph.notes}</p>}
                </div>
              ))}
            </div>
          )}
        </div>
        )}

        {(!only || only === "qc") && (
        <div className="border border-[var(--leon-line)] rounded-md p-2.5">
          <div className="flex items-center justify-between mb-1.5">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">QC Reports ({record.qcReports.length})</p>
            {editable && <Button size="sm" variant="ghost" onClick={() => setQcModal(true)}>+ Add QC Report</Button>}
          </div>
          {record.qcReports.length === 0 ? <EmptyState text="None yet." /> : (
            <div className="space-y-2">
              {[...record.qcReports].sort((a, b) => b.revisionNumber - a.revisionNumber).map(qc => (
                <div key={qc.id} className="text-[11px] border-t border-[var(--leon-line)] first:border-t-0 pt-1.5 first:pt-0">
                  <div className="flex items-center gap-1.5 flex-wrap">
                    <Badge tone={qc.status === 'Current' ? 'green' : 'neutral'}>Rev {qc.revisionNumber} · {qc.status}</Badge>
                    <Badge tone={qc.result === 'Fail' ? 'red' : qc.result === 'Pass with Comments' ? 'yellow' : 'green'}>{qc.result}</Badge>
                    <Badge tone={qc.finalApprovalStatus === 'Approved' ? 'green' : qc.finalApprovalStatus === 'Not Approved' ? 'red' : 'yellow'}>{qc.finalApprovalStatus}</Badge>
                  </div>
                  <p className="text-[var(--leon-black)]/40 mt-0.5">Inspected {fmtDate(qc.inspectionDate)} by {qc.inspector}</p>
                  {qc.issuesFound && <p className="mt-0.5"><span className="font-semibold">Issues:</span> {qc.issuesFound}</p>}
                  {qc.correctiveActionRequired && <p className="mt-0.5"><span className="font-semibold">Corrective Action:</span> {qc.correctiveActionRequired}{qc.correctiveActionDueDate ? ` (due ${fmtDate(qc.correctiveActionDueDate)})` : ''}</p>}
                  {qc.reinspectionRequired && <p className="mt-0.5 text-[var(--leon-black)]/50">Reinspection required{qc.reinspectionDate ? ` — ${fmtDate(qc.reinspectionDate)}` : ''}.</p>}
                  {qc.followUpAssigneeId && <p className="mt-0.5 text-[var(--leon-black)]/50">QC Follow-Up: <strong className="text-[var(--leon-black)]">{personName(ctx.teamDirectory, qc.followUpAssigneeId)}</strong></p>}
                  <div className="mt-0.5"><FileField name={qc.file} url={qc.fileUrl} editable={false} onChange={() => {}} /></div>
                  {qc.supportingPhotos.length > 0 && (
                    <div className="mt-0.5 flex flex-wrap gap-2">
                      {qc.supportingPhotos.map(sp => <FileField key={sp.id} name={sp.file} url={sp.fileUrl} editable={false} onChange={() => {}} />)}
                    </div>
                  )}
                  {qc.notes && <p className="text-[var(--leon-black)]/50 mt-0.5">{qc.notes}</p>}
                </div>
              ))}
            </div>
          )}
        </div>
        )}
      </div>

      <AddProductionDrawingModal open={drawingModal} onClose={() => setDrawingModal(false)} ctx={ctx} project={project} record={record} />
      <AddProductionPhotoModal open={photoModal} onClose={() => setPhotoModal(false)} ctx={ctx} project={project} record={record} />
      <AddProductionQcReportModal open={qcModal} onClose={() => setQcModal(false)} ctx={ctx} project={project} record={record} />
      <ProductionRecordDetailModal open={detailOpen} onClose={() => setDetailOpen(false)} project={project} record={record} scope={scope} shipping={shipping} />
    </div>
  );
}
function ProductionRecordDetailModal({ open, onClose, project, record, scope, shipping }) {
  const history = [
    ...record.drawings.map(d => ({ id: d.id, date: d.revisionDate, user: record.createdBy, reason: `Drawing Rev ${d.revisionNumber} — ${d.status}`, notes: d.notes })),
    ...record.photos.map(p => ({ id: p.id, date: p.date, user: record.createdBy, reason: `Photo${p.item ? ` — ${p.item}` : ''}`, notes: [p.floor && `Floor ${p.floor}`, p.area, p.unit && `Unit ${p.unit}`].filter(Boolean).join(' · ') })),
    ...record.qcReports.map(qc => ({ id: qc.id, date: qc.inspectionDate, user: qc.inspector, reason: `QC Rev ${qc.revisionNumber} — ${qc.result} (${qc.finalApprovalStatus})`, notes: qc.issuesFound })),
  ];
  const attachments = [
    ...record.drawings.filter(d => d.fileUrl).map(d => ({ name: d.file, url: d.fileUrl })),
    ...record.photos.filter(p => p.fileUrl).map(p => ({ name: p.file, url: p.fileUrl })),
    ...record.qcReports.filter(qc => qc.fileUrl).map(qc => ({ name: qc.file, url: qc.fileUrl })),
  ];
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Production Record — ${record.vendorName}`} printable
      fields={[
        { label: 'Project', value: project.name }, { label: 'Scope', value: scope ? scope.name : '—' },
        { label: 'Status', value: record.status }, { label: 'Shipping Approval', value: shipping.approved ? 'Approved' : `Not Approved — ${shipping.reason}` },
        { label: 'Created By', value: record.createdBy }, { label: 'Created Date', value: fmtDate(record.createdDate) },
        { label: 'Drawings', value: record.drawings.length }, { label: 'Photos', value: record.photos.length }, { label: 'QC Reports', value: record.qcReports.length },
      ]}
      attachments={attachments}
      history={history}
    />
  );
}

function AddProductionRecordModal({ open, onClose, ctx, project, editRecord }) {
  const isEdit = !!editRecord;
  const blank = { scopeId: '', vendorId: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (editRecord) setForm({ scopeId: editRecord.scopeId, vendorId: editRecord.vendorId || '', notes: editRecord.notes || '' });
    else setForm({ ...blank, scopeId: project.scopes[0]?.id || '', vendorId: ctx.vendors[0]?.id || '' });
  }, [open, editRecord]);
  // Dates are never entered here — they're read live from that scope's
  // "Production" stage in Scopes & Schedule (productionDateRange, lib.jsx),
  // one database for both, not a second copy that could drift.
  const range = form.scopeId ? productionDateRange(project, form.scopeId) : { start: null, end: null };
  function submit() {
    const vendor = ctx.vendors.find(v => v.id === form.vendorId);
    if (!form.scopeId || !vendor) return;
    if (isEdit) ctx.updateProductionRecord(project.id, editRecord.id, { scopeId: form.scopeId, vendorId: vendor.id, vendorName: vendor.name, notes: form.notes });
    else ctx.addProductionRecord(project.id, form.scopeId, vendor.id, vendor.name);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={isEdit ? `Edit Production Record — ${editRecord.vendorName}` : 'Add Production Record'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Record'}</Button></>}>
      <div className="space-y-3">
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <Field label="Vendor / Manufacturer"><Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}>{ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</Select></Field>
        <Field label="Production Dates" hint="Set on the Scopes & Schedule tab's Production stage — Start/Complete/Report Delay there updates it everywhere, including the Production Timeline.">
          <p className="text-sm">{range.start ? fmtDate(range.start) : 'Not scheduled'} – {range.end ? fmtDate(range.end) : 'Not scheduled'}</p>
        </Field>
        {isEdit && <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>}
      </div>
    </Modal>
  );
}

function AddProductionDrawingModal({ open, onClose, ctx, project, record }) {
  const blank = { revisionDate: todayISO(), notes: '', file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.file) return;
    ctx.addProductionDrawing(project.id, record.id, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Drawing Revision — ${record.vendorName}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Revision</Button></>}>
      <div className="space-y-3">
        <Field label="Revision Date"><TextInput type="date" value={form.revisionDate} onChange={e => setForm({ ...form, revisionDate: e.target.value })} /></Field>
        <Field label="Drawing File" hint="This creates a new, separate revision — it never overwrites a prior upload."><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function AddProductionPhotoModal({ open, onClose, ctx, project, record }) {
  const blank = { date: todayISO(), unit: '', floor: '', area: '', item: '', notes: '', file: '', fileUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.file) return;
    ctx.addProductionPhoto(project.id, record.id, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Production Photo — ${record.vendorName}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Photo</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Item"><TextInput value={form.item} onChange={e => setForm({ ...form, item: e.target.value })} placeholder="e.g. Base Cabinets" /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Unit"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Area"><TextInput value={form.area} onChange={e => setForm({ ...form, area: e.target.value })} /></Field>
        </div>
        <Field label="Photo File"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function AddProductionQcReportModal({ open, onClose, ctx, project, record }) {
  const blank = {
    inspectionDate: todayISO(), inspector: ctx.currentUserName || '', result: 'Pass',
    issuesFound: '', correctiveActionRequired: '', correctiveActionDueDate: '',
    reinspectionRequired: false, reinspectionDate: '', followUpAssigneeId: '',
    finalApprovalStatus: 'Pending', notes: '', file: '', fileUrl: null,
    supportingPhotos: [],
  };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function addSupportingPhoto(fname, url) {
    setForm(f => ({ ...f, supportingPhotos: [...f.supportingPhotos, { id: uid('qcp'), file: fname, fileUrl: url }] }));
  }
  function submit() {
    if (!form.file) return;
    ctx.addProductionQcReport(project.id, record.id, {
      ...form,
      correctiveActionDueDate: form.correctiveActionDueDate || null,
      reinspectionDate: form.reinspectionDate || null,
      followUpAssigneeId: form.followUpAssigneeId || null,
    });
    onClose();
  }
  return (
    <Modal wide open={open} onClose={onClose} title={`Add QC Report — ${record.vendorName}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add QC Report</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Inspection Date"><TextInput type="date" value={form.inspectionDate} onChange={e => setForm({ ...form, inspectionDate: e.target.value })} /></Field>
          <Field label="Inspector"><TextInput value={form.inspector} onChange={e => setForm({ ...form, inspector: e.target.value })} /></Field>
        </div>
        <Field label="QC Result"><Select value={form.result} onChange={e => setForm({ ...form, result: e.target.value })}>{QC_RESULTS.map(r => <option key={r}>{r}</option>)}</Select></Field>
        <Field label="Issues Found"><TextArea rows={2} value={form.issuesFound} onChange={e => setForm({ ...form, issuesFound: e.target.value })} /></Field>
        <Field label="Corrective Action Required"><TextArea rows={2} value={form.correctiveActionRequired} onChange={e => setForm({ ...form, correctiveActionRequired: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Corrective Action Due Date"><TextInput type="date" value={form.correctiveActionDueDate} onChange={e => setForm({ ...form, correctiveActionDueDate: e.target.value })} /></Field>
          <Field label="Reinspection Required">
            <Select value={form.reinspectionRequired ? 'Yes' : 'No'} onChange={e => setForm({ ...form, reinspectionRequired: e.target.value === 'Yes' })}>
              <option>No</option><option>Yes</option>
            </Select>
          </Field>
        </div>
        {form.reinspectionRequired && <Field label="Reinspection Date"><TextInput type="date" value={form.reinspectionDate} onChange={e => setForm({ ...form, reinspectionDate: e.target.value })} /></Field>}
        <Field label="QC Follow-Up Assignee" hint="Who's responsible for chasing corrective action / reinspection.">
          <Select value={form.followUpAssigneeId} onChange={e => setForm({ ...form, followUpAssigneeId: e.target.value })}>
            <option value="">— unassigned —</option>
            {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Final Approval Status">
          <Select value={form.finalApprovalStatus} onChange={e => setForm({ ...form, finalApprovalStatus: e.target.value })}>
            {QC_APPROVAL_STATUSES.map(s => <option key={s}>{s}</option>)}
          </Select>
        </Field>
        <Field label="QC Report File"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Supporting Photos / Attachments">
          <div className="space-y-1.5">
            {form.supportingPhotos.map(sp => <div key={sp.id}><FileField name={sp.file} url={sp.fileUrl} editable={false} onChange={() => {}} /></div>)}
            <FileField name="" url={null} placeholder="Add supporting photo" editable onChange={addSupportingPhoto} />
          </div>
        </Field>
        <Field label="Notes / Comments"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Delivery -------------------------------------------------------------
const DELIVERY_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'inTransit', label: 'In-Transit Containers', icon: '🚢' },
  { key: 'requested', label: 'Pending Approval', icon: '⏳' },
  { key: 'scheduled', label: 'Scheduled Deliveries', icon: '📅' },
  { key: 'packingLists', label: 'Packing Lists', icon: '📦' },
  { key: 'delivered', label: 'Delivered', icon: '✅' },
  { key: 'claims', label: 'Claims', icon: '⚖️' },
];
function DeliveryTab({ ctx, project }) {
  const [sub, setSub] = useState('overview');
  const requested = project.deliveries.filter(d => d.approvalStatus === 'Pending Approval');
  const rejected = project.deliveries.filter(d => d.approvalStatus === 'Rejected');
  const scheduled = project.deliveries.filter(d => d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered');
  const delivered = project.deliveries.filter(d => d.deliveryStatus === 'Delivered');
  const paymentHold = projectPaymentHold(project, 'Deliveries');
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {DELIVERY_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}{t.key === 'requested' && requested.length > 0 ? ` (${requested.length})` : ''}
          </button>
        ))}
      </div>
      <HubTools />
      {paymentHold && (sub === 'requested' || sub === 'scheduled') && (
        <div className="mb-4 border border-[var(--leon-red)] bg-[var(--leon-red)]/5 rounded-lg p-3">
          <p className="text-sm font-bold text-[var(--leon-red)]">⚠ Hold — Payment Not Received</p>
          <p className="text-xs text-[var(--leon-black)]/60 mt-0.5">{paymentHold.description}</p>
        </div>
      )}
      {sub === 'overview' && <DeliveryOverviewSubTab requested={requested} scheduled={scheduled} delivered={delivered} rejected={rejected} />}
      {sub === 'inTransit' && <InTransitContainersSubTab ctx={ctx} project={project} />}
      {sub === 'requested' && <RequestedDeliveriesSubTab ctx={ctx} project={project} deliveries={requested} rejected={rejected} />}
      {sub === 'scheduled' && <ScheduledDeliveriesSubTab ctx={ctx} project={project} deliveries={scheduled} />}
      {sub === 'packingLists' && <DeliveryPackingListsSubTab ctx={ctx} project={project} />}
      {sub === 'delivered' && <DeliveredSubTab ctx={ctx} project={project} deliveries={delivered} />}
      {sub === 'claims' && <DeliveryClaimsSubTab ctx={ctx} project={project} />}
    </div>
  );
}
// Read-only mirror of this project's containers still in transit — no
// parallel data source, just filters the global ctx.exportContainers to
// those with a shipment for this project and reuses ExportContainerCard's
// own display (editable=false hides its edit/claim/receive actions, which
// belong to the Export Hub, not here).
function InTransitContainersSubTab({ ctx, project }) {
  // Widened to also include 'Arrived' — once a container is handed off by
  // Export it stays visible here through receiving, instead of vanishing
  // from Logistics' own view right when it actually needs receiving.
  const containers = containersForProject(ctx.exportContainers, project.id).filter(c => ['In Transit', 'Arrived'].includes(c.status));
  return (
    <div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">Containers from the Export Hub currently in transit or arrived, awaiting receipt — manage document/edit details from Export Hub.</p>
      {containers.length === 0 ? <EmptyState text="No containers currently in transit." /> : (
        <div className="space-y-2">
          {containers.map(c => <ExportContainerCard key={c.id} ctx={ctx} project={project} container={c} editable={false} />)}
        </div>
      )}
    </div>
  );
}
// Promoted out of each Scheduled Delivery card into its own browsable list —
// generation itself still happens from Scheduled Deliveries (it needs a
// specific delivery to attach to), this is just where they're all seen.
function DeliveryPackingListsSubTab({ ctx, project }) {
  const [printFor, setPrintFor] = useState(null);
  const lists = [...ctx.packingLists.filter(pl => pl.projectId === project.id)].sort((a, b) => (a.preparedDate < b.preparedDate ? 1 : -1));
  return (
    <div>
      <Collapsible title="Packing Lists" count={lists.length}>
        {lists.length === 0 ? <EmptyState text="No packing lists generated yet." /> : (
          <div className="space-y-2">
            {lists.map(pl => (
              <div key={pl.id} className="border border-[var(--leon-line)] rounded-lg p-3 cursor-pointer hover:bg-[var(--leon-cream)]" onClick={() => setPrintFor(pl)}>
                <div className="flex items-center justify-between gap-2 flex-wrap">
                  <p className="text-sm font-semibold hover:underline">Packing List {pl.packingListNumber}</p>
                  {pl.deliveryNumber && <Badge tone="neutral">{pl.deliveryNumber}</Badge>}
                </div>
                <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">Prepared by {pl.preparedBy} · {fmtDate(pl.preparedDate)}{pl.plannedDeliveryDate ? ` · Planned ${fmtDate(pl.plannedDeliveryDate)}` : ''}</p>
                <p className="text-xs text-[var(--leon-black)]/40 mt-0.5">{pl.lines.length} line item(s)</p>
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <PackingListPrintModal open={!!printFor} packingList={printFor} onClose={() => setPrintFor(null)} ctx={ctx} />
    </div>
  );
}
// Pulled up from Export/Warehouse into its own subtab — reuses
// ctx.logisticsClaims/AddLogisticsClaimModal exactly as-is, just filtered to
// this project and given a dedicated place to see and log them.
function DeliveryClaimsSubTab({ ctx, project }) {
  const [showAdd, setShowAdd] = useState(false);
  const editable = ctx.canEdit('delivery');
  const claims = [...ctx.logisticsClaims.filter(c => c.projectId === project.id)].sort((a, b) => (a.dateSubmitted < b.dateSubmitted ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowAdd(true)}>+ Log Claim</Button></div>
      <Collapsible title="Claims" count={claims.length}>
        {claims.length === 0 ? <EmptyState text="No claims logged for this project." /> : (
          <div className="space-y-2">
            {claims.map(c => {
              const scope = project.scopes.find(s => s.id === c.scopeId);
              const vendor = ctx.vendors.find(v => v.id === c.vendorId);
              return (
                <div key={c.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-center justify-between gap-2 flex-wrap">
                    <div className="flex items-center gap-2 flex-wrap">
                      <Badge tone="black">{c.claimNumber}</Badge>
                      <p className="text-sm font-semibold">{c.claimType}</p>
                    </div>
                    {editable ? (
                      <Select value={c.status} onChange={e => ctx.setLogisticsClaimStatus(c.id, e.target.value)} className="!py-0.5 !text-xs !w-40">
                        {LOGISTICS_CLAIM_STATUSES.map(s => <option key={s}>{s}</option>)}
                      </Select>
                    ) : <StatusBadge status={c.status} />}
                  </div>
                  <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{scope ? scope.name : 'No scope'}{vendor ? ` · ${vendor.name}` : ''} · {fmtDate(c.dateSubmitted)}</p>
                  {c.description && <p className="text-xs text-[var(--leon-black)]/50 mt-1">{c.description}</p>}
                  {c.replacementRequired && <p className="text-xs text-[var(--leon-black)]/40 mt-1">Replacement: {c.replacementStatus || '—'}{c.replacementEta ? ` · ETA ${fmtDate(c.replacementEta)}` : ''}</p>}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddLogisticsClaimModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} container={null} />
    </div>
  );
}
function DeliveryOverviewSubTab({ requested, scheduled, delivered, rejected }) {
  return (
    <div>
      <div className="grid sm:grid-cols-4 gap-3 mb-5">
        <StatBox label="Requested" value={String(requested.length)} tone={requested.length ? 'yellow' : undefined} />
        <StatBox label="Scheduled" value={String(scheduled.length)} />
        <StatBox label="Delivered" value={String(delivered.length)} tone="green" />
        <StatBox label="Rejected" value={String(rejected.length)} tone={rejected.length ? 'red' : undefined} />
      </div>
      <p className="text-xs text-[var(--leon-black)]/50">A delivery starts as a Request — anyone can submit one — and moves to Scheduled once a Logistic Manager approves it. Generate its Packing List from the Scheduled tab, then confirm receipt to move it to Delivered.</p>
    </div>
  );
}
function RequestedDeliveriesSubTab({ ctx, project, deliveries, rejected }) {
  const [showRequest, setShowRequest] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const [rejectFor, setRejectFor] = useState(null);
  const [scheduleFor, setScheduleFor] = useState(null);
  return (
    <div>
      <div className="flex justify-end mb-2"><Button size="sm" onClick={() => setShowRequest(true)}>+ Request Delivery</Button></div>
      <Collapsible title="Pending Approval" count={deliveries.length}>
        {deliveries.length === 0 ? <EmptyState text="No delivery requests waiting on approval." /> : (
          <div className="space-y-2">
            {deliveries.map(d => {
              const scope = project.scopes.find(s => s.id === d.scopeId);
              return (
                <div key={d.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-start justify-between gap-2 flex-wrap">
                    <div className="min-w-0 cursor-pointer" onClick={() => setDetailFor(d)}>
                      <div className="flex items-center gap-2 flex-wrap">
                        <Badge tone="black">{d.deliveryNumber}</Badge>
                        <p className="text-sm font-semibold hover:underline">{d.description}</p>
                        {d.wantsWarehouseAllocation && <Badge tone="yellow">Wants Warehouse Allocation</Badge>}
                      </div>
                      <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{scope ? scope.name : 'No scope'}{d.areas ? ` · ${d.areas}` : ''}{d.quantity ? ` · ${d.quantity} ${d.unit}` : ''}</p>
                      <p className="text-[11px] text-[var(--leon-black)]/40 mt-0.5">Requested by {d.requestedBy} · {fmtDate(d.requestedDate)}</p>
                      {d.notes && <p className="text-xs text-[var(--leon-black)]/50 mt-1">{d.notes}</p>}
                    </div>
                    {ctx.canApproveDelivery && (
                      <div className="flex gap-1.5 shrink-0">
                        <Button size="sm" onClick={() => setScheduleFor(d)}>Approve</Button>
                        <Button size="sm" variant="ghost" onClick={() => setRejectFor(d)}>Reject</Button>
                      </div>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      {rejected.length > 0 && (
        <Collapsible title="Rejected" count={rejected.length}>
          <div className="space-y-2">
            {rejected.map(d => (
              <div key={d.id} className="border border-[var(--leon-line)] rounded-lg p-3 opacity-70">
                <div className="flex items-center gap-2 flex-wrap"><Badge tone="black">{d.deliveryNumber}</Badge><p className="text-sm font-semibold">{d.description}</p><StatusBadge status="Rejected" /></div>
                <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{d.damageShortageNotes}</p>
              </div>
            ))}
          </div>
        </Collapsible>
      )}
      <RequestDeliveryModal open={showRequest} onClose={() => setShowRequest(false)} ctx={ctx} project={project} />
      <ScheduleDeliveryModal open={!!scheduleFor} delivery={scheduleFor} onClose={() => setScheduleFor(null)} ctx={ctx} project={project} />
      <RejectDeliveryModal open={!!rejectFor} delivery={rejectFor} onClose={() => setRejectFor(null)} ctx={ctx} project={project} />
      <DeliveryDetailModal open={!!detailFor} delivery={detailFor} onClose={() => setDetailFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
// Sums quantity already sitting on a pending-or-approved-but-undelivered
// delivery line against this allocation, across every delivery in the
// project — so two separate delivery requests can never both draw on the
// same un-released warehouse stock at once. Only deliveries with no outcome
// yet count: once a delivery reaches Complete/Partial, ctx.completeDelivery
// has already moved the actually-delivered quantity onto the allocation's
// own quantityReleased, so counting its original planned quantity here too
// would double-subtract it (and Rejected/Cancelled never touched the
// allocation at all).
function allocationQuantityPending(project, allocationId, excludeDeliveryId) {
  return project.deliveries
    .filter(d => d.id !== excludeDeliveryId && d.approvalStatus !== 'Rejected' && d.outcome == null)
    .flatMap(d => d.lines)
    .filter(l => l.allocationId === allocationId)
    .reduce((s, l) => s + (Number(l.quantity) || 0), 0);
}
// `preselectedAllocationId` — set when opened by clicking a specific
// allocation row (Procurement Hub, Inventory hub) rather than the general
// "+ Request Delivery" button — pre-fills that one line's quantity to its
// full remaining amount and the scope to match, so clicking an allocation
// gets you straight to "request this, right now" while still allowing more
// lines to be added in the same request.
function RequestDeliveryModal({ open, onClose, ctx, project, preselectedAllocationId }) {
  const blank = { scopeId: '', description: '', areas: '', date: todayISO(), notes: '', approverId: '', destinationType: 'Jobsite', subcontractorId: '' };
  const [form, setForm] = useState(blank);
  const [qtys, setQtys] = useState({});
  const allocations = ctx.materialAllocations.filter(a => a.projectId === project.id && ['Reserved', 'Partially Released'].includes(a.status));
  function remaining(a) { return (a.quantityAllocated - a.quantityReleased) - allocationQuantityPending(project, a.id, null); }
  useEffect(() => {
    if (open) {
      const preselected = preselectedAllocationId ? allocations.find(a => a.id === preselectedAllocationId) : null;
      setForm({ ...blank, approverId: teamMemberFor(project, 'Logistic Manager') || '', scopeId: preselected ? (preselected.scopeId || '') : '' });
      setQtys(preselected ? { [preselected.id]: String(remaining(preselected)) } : {});
    }
  }, [open, preselectedAllocationId]);
  const lines = allocations.map(a => ({ alloc: a, qty: Number(qtys[a.id]) || 0 })).filter(x => x.qty > 0);
  function submit() {
    if (!lines.length && !form.description.trim()) return;
    const builtLines = lines.map(({ alloc, qty }) => {
      const m = ctx.warehouseMaterials.find(x => x.id === alloc.materialId);
      return { id: uid('dline'), allocationId: alloc.id, materialId: alloc.materialId, itemName: m ? m.name : alloc.materialId, quantity: qty, unit: alloc.unitOfMeasure, scopeId: form.scopeId || null, notes: '', deliveredQuantity: null };
    });
    const description = form.description.trim() || `${builtLines.length} material line(s)`;
    ctx.requestDelivery(project.id, { ...form, description, scopeId: form.scopeId || null, approverId: form.approverId || null, lines: builtLines });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title="Request Delivery" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit Request</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">A delivery number is generated automatically. Your assigned approver will need to approve this before it's scheduled.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
          <Field label="Needed By"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Approver" hint="Defaults to this project's Logistic Manager"><Select value={form.approverId} onChange={e => setForm({ ...form, approverId: e.target.value })}><option value="">— select —</option>{ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
        <Field label="Material — select from what's allocated to this project" hint="Enter a quantity for each item you want delivered">
          {allocations.length === 0 ? <EmptyState text="No warehouse material is allocated to this project yet." /> : (
            <div className="border border-[var(--leon-line)] rounded-lg divide-y divide-[var(--leon-line)]">
              {allocations.map(a => {
                const m = ctx.warehouseMaterials.find(x => x.id === a.materialId);
                const avail = remaining(a);
                return (
                  <div key={a.id} className="p-2 flex items-center justify-between gap-2 text-xs">
                    <span>{m ? m.name : a.materialId} — {avail} {a.unitOfMeasure} available</span>
                    <TextInput type="number" className="!w-24 !py-1 !text-xs" value={qtys[a.id] || ''} onChange={e => setQtys({ ...qtys, [a.id]: Math.min(Number(e.target.value) || 0, avail) })} placeholder="Qty" disabled={avail <= 0} />
                  </div>
                );
              })}
            </div>
          )}
        </Field>
        <Field label="Description" hint="Optional — auto-filled from the material lines above if left blank"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="What needs to be delivered" /></Field>
        {/* Material often goes to a subcontractor's shop for pre-fabrication
            rather than straight to site; that had nowhere to be recorded. */}
        <div className="grid grid-cols-2 gap-3">
          <Field label="Deliver to">
            <Select value={form.destinationType} onChange={e => setForm({ ...form, destinationType: e.target.value, subcontractorId: e.target.value === 'Subcontractor' ? form.subcontractorId : '' })}>
              {DELIVERY_DESTINATIONS.map(d => <option key={d} value={d}>{d}</option>)}
            </Select>
          </Field>
          {form.destinationType === 'Subcontractor' && (
            <Field label="Which subcontractor" hint="Their shop address is used as the delivery address.">
              <Select value={form.subcontractorId} onChange={e => setForm({ ...form, subcontractorId: e.target.value })}>
                <option value="">— select —</option>
                {(ctx.subcontractors || []).filter(sc => sc.active !== false).map(sc => <option key={sc.id} value={sc.id}>{sc.name}{sc.trade ? ` — ${sc.trade}` : ''}</option>)}
              </Select>
            </Field>
          )}
        </div>
        <Field label="Areas" hint="e.g. Primary Bath Shower Surround, Powder Room Floor"><TextInput value={form.areas} onChange={e => setForm({ ...form, areas: e.target.value })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
// Shared by "Approve" (first schedule, on a Pending Approval request) and
// "Reschedule" (changing an already-Approved delivery's schedule) — per the
// client's own description, both pop open the exact same window.
function ScheduleDeliveryModal({ open, delivery, onClose, ctx, project }) {
  const blank = { date: '', deliveryTime: '', durationMinutes: '', driverId: '', helperId: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (open && delivery) {
      setForm({
        date: delivery.date || todayISO(), deliveryTime: delivery.deliveryTime || '',
        durationMinutes: delivery.durationMinutes || '', driverId: delivery.driverId || '', helperId: delivery.helperId || '',
      });
    }
  }, [open, delivery]);
  if (!delivery) return null;
  const contact = project.contacts['Jobsite Delivery Contact'] || { company: '', person: '', phone: '', email: '' };
  const contactLine = [contact.person, contact.company, contact.phone].filter(Boolean).join(' · ') || 'No jobsite delivery contact on file — add one under the Contacts tab.';
  const drivers = ctx.teamDirectory.filter(p => p.active && p.securityRole === 'Delivery Driver');
  const helpers = ctx.teamDirectory.filter(p => p.active);
  // Truck maintenance (Phase 8) — a soft warning, not a hard block, keyed
  // off whichever truck lists this driver as its assigned driver.
  const truckOnMaintenance = form.driverId && form.date
    ? ctx.trucks.find(t => t.assignedDriverId === form.driverId && truckUnderMaintenanceOn(t, form.date))
    : null;
  function submit() {
    ctx.scheduleDelivery(project.id, delivery.id, {
      date: form.date, deliveryTime: form.deliveryTime, durationMinutes: form.durationMinutes,
      driverId: form.driverId || null, helperId: form.helperId || null, jobsiteContact: contactLine,
      destinationType: form.destinationType,
      subcontractorId: form.destinationType === 'Subcontractor' ? (form.subcontractorId || null) : null,
      deliveryAddress: form.destinationType === 'Subcontractor'
        ? (((ctx.subcontractors || []).find(sc => sc.id === form.subcontractorId) || {}).address || '')
        : (project.address || ''),
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Schedule — ${delivery.deliveryNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Schedule</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Delivery Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.deliveryTime} onChange={e => setForm({ ...form, deliveryTime: e.target.value })} /></Field>
        </div>
        <Field label="Estimated Duration (minutes)"><TextInput type="number" value={form.durationMinutes} onChange={e => setForm({ ...form, durationMinutes: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Delivery Driver"><Select value={form.driverId} onChange={e => setForm({ ...form, driverId: e.target.value })}><option value="">— select —</option>{drivers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
          <Field label="Helper"><Select value={form.helperId} onChange={e => setForm({ ...form, helperId: e.target.value })}><option value="">—</option>{helpers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
        </div>
        {truckOnMaintenance && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ {truckOnMaintenance.name} is under maintenance on this date — consider a different driver or date.</p>}
        <Field label="Jobsite Contact" hint="From this project's Contacts tab"><p className="text-sm border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-[var(--leon-cream)]">{contactLine}</p></Field>
      </div>
    </Modal>
  );
}
function RejectDeliveryModal({ open, delivery, onClose, ctx, project }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  if (!delivery) return null;
  function submit() { ctx.rejectDeliveryRequest(project.id, delivery.id, reason); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Reject — ${delivery.deliveryNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit}>Reject Request</Button></>}>
      <Field label="Reason (optional)"><TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} /></Field>
    </Modal>
  );
}
function ScheduledDeliveriesSubTab({ ctx, project }) {
  const editable = ctx.canEdit('delivery');
  const [showAdd, setShowAdd] = useState(false);
  const [completeFor, setCompleteFor] = useState(null);
  const [confirmFor, setConfirmFor] = useState(null);
  const [detailFor, setDetailFor] = useState(null);
  const [lineFor, setLineFor] = useState(null);
  const [genError, setGenError] = useState(null);
  const [editFor, setEditFor] = useState(null);
  const [scheduleFor, setScheduleFor] = useState(null);
  async function generatePL(deliveryId) {
    const res = await ctx.generateDeliveryPackingList(project.id, deliveryId);
    setGenError(res && res.error ? { deliveryId, message: res.error } : null);
  }
  const deliveries = project.deliveries.filter(d => d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered');
  const sorted = [...deliveries].sort((a, b) => (a.date < b.date ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Log Delivery</Button>}</div>
      <Collapsible title="Scheduled Deliveries" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No scheduled deliveries." /> : (
          <div className="space-y-2">
            {sorted.map(d => {
              const scope = project.scopes.find(s => s.id === d.scopeId);
              const pl = d.packingListId ? ctx.packingLists.find(p => p.id === d.packingListId) : null;
              return (
                <div key={d.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-start justify-between gap-3">
                    <div className="min-w-0 cursor-pointer" onClick={() => setDetailFor(d)}>
                      <div className="flex items-center gap-2 flex-wrap">
                        <Badge tone="black">{d.deliveryNumber}</Badge>
                        <p className="text-sm font-semibold hover:underline">{d.description}</p>
                        {ctx.canEditDeliveryStatus ? (
                          <Select value={d.deliveryStatus} onClick={e => e.stopPropagation()} onChange={e => ctx.updateDelivery(project.id, d.id, { deliveryStatus: e.target.value })} className="!py-0.5 !text-xs !w-32">
                            {DELIVERY_STATUSES.map(s => <option key={s}>{s}</option>)}
                          </Select>
                        ) : <StatusBadge status={d.deliveryStatus} />}
                        {pl && <Badge tone="neutral">Packing List {pl.packingListNumber}</Badge>}
                      </div>
                      <p className="text-xs text-[var(--leon-black)]/50">{fmtDate(d.date)}{d.deliveryTime ? ` ${d.deliveryTime}` : ''}{scope ? ` · ${scope.name}` : ''}{d.carrier ? ` · ${d.carrier}` : ''}</p>
                      {(d.driverId || d.helperId) && (
                        <p className="text-xs text-[var(--leon-black)]/50">Driver: {d.driverId ? personName(ctx.teamDirectory, d.driverId) : '—'}{d.helperId ? ` · Helper: ${personName(ctx.teamDirectory, d.helperId)}` : ''}{d.durationMinutes ? ` · ~${d.durationMinutes} min` : ''}</p>
                      )}
                      {d.notes && <p className="text-xs text-[var(--leon-black)]/50 mt-1">{d.notes}</p>}
                    </div>
                    <div className="flex flex-col items-end gap-1 shrink-0">
                      {d.jobsitePhoto ? <ClickableImage src={d.jobsitePhoto} name="Jobsite Photo" className="w-16 h-16 object-cover rounded-md" /> : <div className="w-16 h-16 rounded-md bg-[var(--leon-cream)] flex items-center justify-center text-[var(--leon-black)]/25">🖼</div>}
                      {ctx.canEditDeliveryStatus && <button onClick={() => setEditFor(d)} className="text-xs text-[var(--leon-brown)] font-semibold">✎ Edit</button>}
                    </div>
                  </div>

                  <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
                    <div className="flex items-center justify-between mb-1">
                      <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">Line Items</p>
                      {editable && <button onClick={() => setLineFor(d)} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Line Item</button>}
                    </div>
                    {d.lines.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40 italic">No manual line items yet.</p> : (
                      <div className="space-y-1">
                        {d.lines.map(l => {
                          const lscope = project.scopes.find(s => s.id === l.scopeId);
                          return (
                            <div key={l.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded px-2 py-1">
                              <span>{l.quantity} {l.unit} — {l.itemName} <span className="text-[var(--leon-black)]/40">({lscope ? lscope.name : 'no scope'})</span></span>
                              {editable && <button onClick={() => ctx.removeDeliveryLine(project.id, d.id, l.id)} className="text-[var(--leon-red)] font-semibold">✕</button>}
                            </div>
                          );
                        })}
                      </div>
                    )}
                  </div>

                  {editable && (
                    <div className="mt-2 flex flex-col gap-1.5">
                      <div className="flex items-center gap-2 flex-wrap">
                        {!pl && <Button size="sm" variant="outline" onClick={() => generatePL(d.id)}>Generate Packing List</Button>}
                        <Button size="sm" variant="ghost" onClick={() => setScheduleFor(d)}>Reschedule</Button>
                        <Button size="sm" variant="ghost" onClick={() => setCompleteFor(d)}>Complete Delivery Details</Button>
                        <Button size="sm" variant="outline" onClick={() => setConfirmFor(d)}>Confirm Delivered</Button>
                      </div>
                      {genError && genError.deliveryId === d.id && <p className="text-xs text-[var(--leon-red)]">{genError.message}</p>}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddDeliveryModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <CompleteDeliveryModal open={!!completeFor} delivery={completeFor} onClose={() => setCompleteFor(null)} ctx={ctx} project={project} />
      <ConfirmDeliveredModal open={!!confirmFor} delivery={confirmFor} onClose={() => setConfirmFor(null)} ctx={ctx} project={project} />
      <AddDeliveryLineModal open={!!lineFor} delivery={lineFor} onClose={() => setLineFor(null)} ctx={ctx} project={project} />
      <DeliveryDetailModal open={!!detailFor} delivery={detailFor} onClose={() => setDetailFor(null)} ctx={ctx} project={project} />
      <EditDeliveryModal open={!!editFor} delivery={editFor} onClose={() => setEditFor(null)} ctx={ctx} project={project} />
      <ScheduleDeliveryModal open={!!scheduleFor} delivery={scheduleFor} onClose={() => setScheduleFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
// Direct field/status editing — separate from the request/approve/generate-
// packing-list/confirm workflow buttons above, and gated tighter (Admin/
// Accounting/Logistic Manager only, per explicit instruction) since this
// lets someone correct a delivery record after the fact, including
// reverting its status, rather than only ever moving it forward.
function EditDeliveryModal({ open, delivery, onClose, ctx, project }) {
  const blank = { description: '', date: '', deliveryStatus: 'Pending', areas: '', quantity: '', unit: 'Units', carrier: '', receiverName: '', receiverPhone: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (open && delivery) {
      setForm({
        description: delivery.description, date: delivery.date, deliveryStatus: delivery.deliveryStatus,
        areas: delivery.areas || '', quantity: delivery.quantity ?? '', unit: delivery.unit || 'Units',
        carrier: delivery.carrier || '', receiverName: delivery.receiverName || '', receiverPhone: delivery.receiverPhone || '', notes: delivery.notes || '',
      });
    }
  }, [open, delivery]);
  if (!delivery) return null;
  function submit() {
    ctx.updateDelivery(project.id, delivery.id, { ...form, quantity: form.quantity === '' ? null : Number(form.quantity) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Edit Delivery — ${delivery.deliveryNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Changes</Button></>}>
      <div className="space-y-3">
        <Field label="Description"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Delivery Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Status">
            <Select value={form.deliveryStatus} onChange={e => setForm({ ...form, deliveryStatus: e.target.value })}>
              {DELIVERY_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Quantity"><TextInput type="number" value={form.quantity} onChange={e => setForm({ ...form, quantity: e.target.value })} /></Field>
          <Field label="Unit"><Select value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })}>{UNIT_TYPES.map(u => <option key={u}>{u}</option>)}</Select></Field>
          <Field label="Carrier"><TextInput value={form.carrier} onChange={e => setForm({ ...form, carrier: e.target.value })} /></Field>
        </div>
        <Field label="Areas"><TextInput value={form.areas} onChange={e => setForm({ ...form, areas: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Receiver Name"><TextInput value={form.receiverName} onChange={e => setForm({ ...form, receiverName: e.target.value })} /></Field>
          <Field label="Receiver Phone"><TextInput value={form.receiverPhone} onChange={e => setForm({ ...form, receiverPhone: e.target.value })} /></Field>
        </div>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddDeliveryLineModal({ open, delivery, onClose, ctx, project }) {
  const blank = { scopeId: '', itemName: '', quantity: '', unit: 'Units', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  if (!delivery) return null;
  function submit() { if (!form.itemName.trim() || !form.scopeId) return; ctx.addDeliveryLine(project.id, delivery.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Add Line Item — ${delivery.deliveryNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Item"><TextInput value={form.itemName} onChange={e => setForm({ ...form, itemName: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Quantity"><TextInput type="number" value={form.quantity} onChange={e => setForm({ ...form, quantity: e.target.value })} /></Field>
          <Field label="Unit"><Select value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })}>{UNIT_TYPES.map(u => <option key={u}>{u}</option>)}</Select></Field>
        </div>
        <Field label="Scope" hint="Required — every line item is tied to a scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function ConfirmDeliveredModal({ open, delivery, onClose, ctx, project }) {
  const blank = { receiverName: '', receiverPhone: '', clientSignatureUrl: null };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  if (!delivery) return null;
  function submit() { ctx.confirmDeliveryReceived(project.id, delivery.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Confirm Delivered — ${delivery.deliveryNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Confirm Delivered</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Receiver Name"><TextInput value={form.receiverName} onChange={e => setForm({ ...form, receiverName: e.target.value })} /></Field>
          <Field label="Receiver Phone"><TextInput value={form.receiverPhone} onChange={e => setForm({ ...form, receiverPhone: e.target.value })} /></Field>
        </div>
        <div>
          <p className="text-xs font-semibold mb-1">Client Signature</p>
          <ImagePicker url={form.clientSignatureUrl} onChange={url => setForm({ ...form, clientSignatureUrl: url })} size={80} />
        </div>
      </div>
    </Modal>
  );
}
function DeliveredSubTab({ ctx, project }) {
  const [detailFor, setDetailFor] = useState(null);
  const [editFor, setEditFor] = useState(null);
  const deliveries = project.deliveries.filter(d => d.deliveryStatus === 'Delivered');
  const sorted = [...deliveries].sort((a, b) => (a.date < b.date ? 1 : -1));
  return (
    <div>
      <Collapsible title="Delivered" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="Nothing delivered yet." /> : (
          <div className="space-y-2">
            {sorted.map(d => {
              const scope = project.scopes.find(s => s.id === d.scopeId);
              const pl = d.packingListId ? ctx.packingLists.find(p => p.id === d.packingListId) : null;
              return (
                <div key={d.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-start justify-between gap-2">
                    <div className="cursor-pointer flex-1 min-w-0" onClick={() => setDetailFor(d)}>
                      <div className="flex items-center gap-2 flex-wrap">
                        <Badge tone="black">{d.deliveryNumber}</Badge>
                        <p className="text-sm font-semibold hover:underline">{d.description}</p>
                        <Badge tone="green">Delivered</Badge>
                        {pl && <Badge tone="neutral">Packing List {pl.packingListNumber}</Badge>}
                      </div>
                      <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{fmtDate(d.date)}{scope ? ` · ${scope.name}` : ''}{d.receiverName ? ` · Received by ${d.receiverName}` : ''}</p>
                    </div>
                    {ctx.canEditDeliveryStatus && <button onClick={() => setEditFor(d)} className="text-xs text-[var(--leon-brown)] font-semibold shrink-0">✎ Edit</button>}
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <EditDeliveryModal open={!!editFor} delivery={editFor} onClose={() => setEditFor(null)} ctx={ctx} project={project} />
      <DeliveryDetailModal open={!!detailFor} delivery={detailFor} onClose={() => setDetailFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function CompleteDeliveryModal({ open, delivery, onClose, ctx, project }) {
  // Simplified to match how deliveries actually run here — LEON's own
  // trucks and staff, per the real packing-list format (Prepared By /
  // Delivery = "DELIVERY" / a named internal driver — no carrier, trucking
  // company, vehicle, tracking/BOL number, or bill of lading involved).
  // Jobsite Contact and Delivery Address are no longer freehand — they
  // always come from the project itself.
  const blank = { driverId: '', deliveryTime: '', proofOfDeliveryUrl: null, signedPackingListUrl: null, damageShortageNotes: '', deliveryPictures: [] };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open && delivery) setForm({ ...blank, ...delivery, driverId: delivery.driverId || '' }); }, [open, delivery]);
  if (!delivery) return null;
  const contact = project.contacts['Jobsite Delivery Contact'] || { company: '', person: '', phone: '', email: '' };
  const contactLine = [contact.person, contact.company, contact.phone].filter(Boolean).join(' · ') || 'No jobsite delivery contact on file — add one under the Contacts tab.';
  const drivers = ctx.teamDirectory.filter(p => p.active && p.securityRole === 'Delivery Driver');
  function submit() { ctx.updateDelivery(project.id, delivery.id, { ...form, driverId: form.driverId || null }); onClose(); }
  function addPic(url) { setForm({ ...form, deliveryPictures: [...form.deliveryPictures, url] }); }
  function removePic(i) { setForm({ ...form, deliveryPictures: form.deliveryPictures.filter((_, idx) => idx !== i) }); }
  return (
    <Modal open={open} onClose={onClose} wide title="Complete Delivery Details" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Driver"><Select value={form.driverId} onChange={e => setForm({ ...form, driverId: e.target.value })}><option value="">— select —</option>{drivers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
          <Field label="Delivery Time"><TextInput type="time" value={form.deliveryTime} onChange={e => setForm({ ...form, deliveryTime: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Jobsite Contact"><p className="text-sm border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-[var(--leon-cream)]">{contactLine}</p></Field>
          <Field label="Delivery Address"><p className="text-sm border border-[var(--leon-line)] rounded-lg px-3 py-2 bg-[var(--leon-cream)]">{project.address || '—'}</p></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Proof of Delivery"><FileField name={form.proofOfDeliveryUrl ? 'Proof of Delivery' : ''} url={form.proofOfDeliveryUrl} onChange={(fname, url) => setForm({ ...form, proofOfDeliveryUrl: url })} editable /></Field>
          <Field label="Signed Packing List"><FileField name={form.signedPackingListUrl ? 'Signed Packing List' : ''} url={form.signedPackingListUrl} onChange={(fname, url) => setForm({ ...form, signedPackingListUrl: url })} editable /></Field>
        </div>
        <div>
          <p className="text-xs font-semibold mb-1">Delivery Pictures</p>
          <div className="flex items-center gap-2 flex-wrap">
            {form.deliveryPictures.map((p, i) => (
              <span key={i} className="relative">
                <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                <button type="button" onClick={() => removePic(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
              </span>
            ))}
            <ImagePicker url={null} onChange={addPic} size={56} />
          </div>
        </div>
        <Field label="Damage / Shortage / Discrepancy Notes"><TextArea rows={2} value={form.damageShortageNotes} onChange={e => setForm({ ...form, damageShortageNotes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function DeliveryDetailModal({ open, delivery, onClose, ctx, project }) {
  if (!delivery) return null;
  const scope = project.scopes.find(s => s.id === delivery.scopeId);
  const pl = delivery.packingListId ? ctx.packingLists.find(p => p.id === delivery.packingListId) : null;
  const release = delivery.warehouseReleaseId ? ctx.warehouseReleases.find(r => r.id === delivery.warehouseReleaseId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Delivery ${delivery.deliveryNumber} — ${delivery.description}`} printable
      fields={[
        { label: 'Approval Status', value: delivery.approvalStatus }, { label: 'Delivery Status', value: delivery.deliveryStatus },
        { label: 'Scope', value: scope ? scope.name : '—' }, { label: 'Areas', value: delivery.areas },
        { label: 'Quantity', value: delivery.quantity ? `${delivery.quantity} ${delivery.unit}` : '—' },
        { label: 'Date', value: fmtDate(delivery.date) }, { label: 'Time', value: delivery.deliveryTime || '—' },
        { label: 'Requested By', value: delivery.requestedBy ? `${delivery.requestedBy} — ${fmtDate(delivery.requestedDate)}` : '—' },
        { label: 'Approved By', value: delivery.approvedBy ? `${delivery.approvedBy} — ${fmtDate(delivery.approvedDate)}` : '—' },
        { label: 'From Packing List', value: pl ? pl.packingListNumber : '—' }, { label: 'From Release', value: release ? release.releaseNumber : '—' },
        { label: 'Approver', value: personName(ctx.teamDirectory, delivery.approverId) },
        { label: 'Assigned Driver', value: personName(ctx.teamDirectory, delivery.driverId) }, { label: 'Helper', value: personName(ctx.teamDirectory, delivery.helperId) },
        { label: 'Estimated Duration', value: delivery.durationMinutes ? `${delivery.durationMinutes} min` : '—' },
        { label: 'Carrier', value: delivery.carrier }, { label: 'Driver (free text)', value: delivery.driver },
        { label: 'Vehicle Info', value: delivery.vehicleInfo }, { label: 'Tracking / BOL Number', value: delivery.trackingBolNumber },
        { label: 'Destination', value: delivery.destinationType === 'Subcontractor'
            ? `Subcontractor — ${(ctx.subcontractors.find(sc => sc.id === delivery.subcontractorId) || {}).name || 'unnamed'}`
            : (delivery.destinationType || 'Jobsite') },
        { label: 'Jobsite Contact', value: delivery.jobsiteContact }, { label: 'Delivery Address', value: delivery.deliveryAddress },
        { label: 'Receiver Name', value: delivery.receiverName }, { label: 'Receiver Phone', value: delivery.receiverPhone },
        { label: 'Outcome', value: delivery.outcome || '—' }, { label: 'Cancel Reason', value: delivery.cancelReason || '—' },
        { label: 'Damage / Shortage / Discrepancy', value: delivery.damageShortageNotes }, { label: 'Notes', value: delivery.notes }, { label: 'Driver Notes', value: delivery.driverNotes },
      ]}
      attachments={[
        ...(delivery.slipFileUrl ? [{ name: delivery.slipFile || 'Delivery Slip', url: delivery.slipFileUrl }] : []),
        ...(delivery.proofOfDeliveryUrl ? [{ name: 'Proof of Delivery', url: delivery.proofOfDeliveryUrl }] : []),
        ...(delivery.signedPackingListUrl ? [{ name: 'Signed Packing List', url: delivery.signedPackingListUrl }] : []),
        ...(delivery.billOfLadingUrl ? [{ name: 'Bill of Lading', url: delivery.billOfLadingUrl }] : []),
        ...(delivery.clientSignatureUrl ? [{ name: 'Client Signature', url: delivery.clientSignatureUrl }] : []),
        ...(delivery.deliveryPictures || []).map((p, i) => ({ name: `Photo ${i + 1}`, url: p })),
      ]}
    >
      {delivery.lines.length > 0 && (
        <div>
          <p className="text-xs font-semibold mb-1.5">Line Items</p>
          {/* One delivery routinely covers several scopes — grouping by scope
              makes the load legible instead of one undifferentiated run. */}
          <div className="space-y-2">
            {(() => {
              const groups = [];
              delivery.lines.forEach(l => {
                const sc = project.scopes.find(x => x.id === l.scopeId);
                const key = sc ? sc.id : 'none';
                let g = groups.find(x => x.key === key);
                if (!g) { g = { key, name: sc ? sc.name : 'No scope', lines: [] }; groups.push(g); }
                g.lines.push(l);
              });
              return groups.map(g => (
                <div key={g.key} className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
                  <div className="bg-[var(--leon-cream)] px-2.5 py-1 flex items-center gap-2">
                    <span className="text-[10px] font-bold uppercase tracking-wide text-[var(--leon-brown)]">{g.name}</span>
                    <span className="text-[10px] text-[var(--leon-black)]/40 ml-auto">{g.lines.length} item{g.lines.length === 1 ? '' : 's'}</span>
                  </div>
                  {g.lines.map(l => (
                    <p key={l.id} className="text-xs text-[var(--leon-black)]/70 px-2.5 py-1 border-t border-[var(--leon-line)] flex items-baseline gap-2">
                      <span className="font-semibold tabular-nums shrink-0">{l.quantity} {l.unit}</span>
                      <span className="min-w-0 flex-1">{l.itemName}{l.notes ? ` — ${l.notes}` : ''}</span>
                      {l.deliveredQuantity != null && <span className="text-[10px] text-[var(--leon-black)]/40 shrink-0">delivered {l.deliveredQuantity}</span>}
                    </p>
                  ))}
                </div>
              ));
            })()}
          </div>
        </div>
      )}
    </RecordDetailModal>
  );
}
function AddDeliveryModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ scopeId: '', description: '', date: todayISO(), slipFile: '', slipFileUrl: null, jobsitePhoto: null, notes: '' });
  useEffect(() => { if (open) setForm({ scopeId: '', description: '', date: todayISO(), slipFile: '', slipFileUrl: null, jobsitePhoto: null, notes: '' }); }, [open]);
  function submit() { if (!form.description.trim()) return; ctx.addDelivery(project.id, { ...form, scopeId: form.scopeId || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Log Delivery" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Log Delivery</Button></>}>
      <div className="space-y-3">
        <div className="flex items-center gap-3">
          <ImagePicker url={form.jobsitePhoto} onChange={url => setForm({ ...form, jobsitePhoto: url })} size={64} />
          <p className="text-xs text-[var(--leon-black)]/50">Jobsite photo</p>
        </div>
        <Field label="Description"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="What was delivered" /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Related Scope (optional)"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
        <Field label="Delivery Slip"><FileField name={form.slipFile} url={form.slipFileUrl} onChange={(fname, url) => setForm({ ...form, slipFile: fname, slipFileUrl: url })} editable /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Project Material Journey — PO/PI -> Production -> ... -> POD --------
// Deliberately NOT a stored/editable stage list: every stage is derived live
// from the exact same PO/PI, Production, Export Container, Allocation, and
// Delivery records the rest of the app already edits, so there is nothing
// here that can drift out of sync with reality. "Planned" is only ever a
// real tracked date (PO issue date, container ETD/ETA, etc) — where this
// app genuinely doesn't track a planned date for a stage yet, it shows "—"
// rather than inventing one.
const MATERIAL_JOURNEY_STAGES = [
  { key: 'po_pi', label: 'PO/PI' }, { key: 'production', label: 'Production' }, { key: 'production_complete', label: 'Production Complete' },
  { key: 'ready_to_ship', label: 'Ready to Ship' }, { key: 'booking', label: 'Booking' }, { key: 'etd', label: 'ETD' },
  { key: 'in_transit', label: 'In Transit' }, { key: 'eta', label: 'ETA' }, { key: 'port_arrival', label: 'Port Arrival' },
  { key: 'customs', label: 'Customs' }, { key: 'warehouse_received', label: 'Warehouse Received' }, { key: 'inventory_allocated', label: 'Inventory Allocated' },
  { key: 'released', label: 'Released' }, { key: 'delivery_scheduled', label: 'Delivery Scheduled' }, { key: 'delivered', label: 'Delivered' }, { key: 'pod', label: 'POD' },
];
function deriveScopeJourney(ctx, project, scope) {
  const po = (project.purchaseOrders || []).find(x => (project.vendorEstimates.find(v => v.id === x.vendorEstimateId) || {}).scopeId === scope.id);
  const pi = (project.proformaInvoices || []).find(x => x.scopeId === scope.id);
  const prodRecords = (project.productionRecords || []).filter(r => r.scopeId === scope.id);
  const latestProd = prodRecords.length ? [...prodRecords].sort((a, b) => (a.createdDate < b.createdDate ? 1 : -1))[0] : null;
  const prodComplete = prodRecords.find(r => r.status === 'Complete');
  const containers = containersForProjectScope(ctx.exportContainers, project.id, scope.id);
  const soonestContainer = containers.length ? [...containers].sort((a, b) => ((a.etd || '9999') < (b.etd || '9999') ? -1 : 1))[0] : null;
  const arrivedContainer = containers.find(c => c.status === 'Arrived' || c.status === 'Delivered');
  const allocations = ctx.materialAllocations.filter(a => a.projectId === project.id && a.scopeId === scope.id && a.status !== 'Cancelled');
  const released = allocations.filter(a => a.quantityReleased > 0);
  const deliveries = (project.deliveries || []).filter(d => d.scopeId === scope.id);
  const scheduledDelivery = deliveries.find(d => d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered');
  const deliveredDelivery = deliveries.find(d => d.deliveryStatus === 'Delivered');
  const today = todayISO();
  const responsibleExport = personName(ctx.teamDirectory, teamMemberFor(project, 'Export Manager')) || '—';
  const responsibleLogistics = personName(ctx.teamDirectory, teamMemberFor(project, 'Logistic Manager')) || '—';

  function build(key, label, planned, actual, complete, inProgress, responsible, notes, attachments) {
    let status = complete ? 'Complete' : inProgress ? 'In Progress' : 'Not Started';
    if (status !== 'Complete' && planned && planned < today) status = 'Delayed';
    return { key, label, planned: planned || null, actual: actual || null, status, responsible: responsible || '—', notes: notes || '', attachments: attachments || [] };
  }

  return [
    build('po_pi', 'PO/PI', po ? po.issuedDate : null, pi ? pi.piDate : null, !!pi, !!po && !pi,
      '—', po ? `PO ${po.poNumber}${pi ? `, PI ${pi.piNumber}` : ''}` : '', pi && pi.fileUrl ? [{ name: pi.file, url: pi.fileUrl }] : []),
    build('production', 'Production', null, latestProd ? latestProd.createdDate : null, !!latestProd && latestProd.status !== 'Not Started', false,
      latestProd ? latestProd.vendorName : '—', latestProd ? `Status: ${latestProd.status}` : '', (latestProd && latestProd.photos) || []),
    build('production_complete', 'Production Complete', null, prodComplete ? prodComplete.createdDate : null, !!prodComplete, !prodComplete && !!latestProd,
      prodComplete ? prodComplete.vendorName : '—', '', (prodComplete && prodComplete.qcReports) || []),
    build('ready_to_ship', 'Ready to Ship', null, containers.length ? containers[0].createdDate : null, containers.length > 0, false, responsibleExport, containers.length ? `${containers.length} container(s)` : ''),
    build('booking', 'Booking', null, null, !!(soonestContainer && soonestContainer.bookingNumber), !!soonestContainer && !soonestContainer.bookingNumber,
      responsibleExport, soonestContainer ? soonestContainer.bookingNumber || 'Not yet booked' : ''),
    build('etd', 'ETD', soonestContainer ? soonestContainer.etd : null, soonestContainer ? soonestContainer.actualDeparture : null, !!(soonestContainer && soonestContainer.actualDeparture), false, responsibleExport, ''),
    build('in_transit', 'In Transit', null, null, containers.some(c => ['Arrived', 'Delivered'].includes(c.status)), containers.some(c => c.status === 'In Transit'), responsibleExport, ''),
    build('eta', 'ETA', soonestContainer ? soonestContainer.eta : null, soonestContainer ? soonestContainer.actualArrival : null, !!(soonestContainer && soonestContainer.actualArrival), false, responsibleLogistics, ''),
    build('port_arrival', 'Port Arrival', null, arrivedContainer ? arrivedContainer.actualArrival : null, !!arrivedContainer, false, responsibleLogistics, ''),
    build('customs', 'Customs', null, null, containers.some(c => c.customsStatus === 'Cleared'), containers.some(c => c.customsStatus && !['Not Started', 'Cleared'].includes(c.customsStatus)),
      responsibleLogistics, containers.length ? [...new Set(containers.map(c => c.customsStatus))].join(', ') : ''),
    build('warehouse_received', 'Warehouse Received', null, arrivedContainer ? arrivedContainer.actualPickupDate : null, !!(arrivedContainer && arrivedContainer.actualPickupDate), !!arrivedContainer && !arrivedContainer.actualPickupDate, responsibleLogistics, ''),
    build('inventory_allocated', 'Inventory Allocated', null, allocations.length ? allocations[0].allocationDate : null, allocations.length > 0, false, allocations[0] ? allocations[0].allocatedBy : '—', allocations.length ? `${allocations.length} allocation(s)` : ''),
    build('released', 'Released', null, null, released.length > 0, false, allocations[0] ? allocations[0].allocatedBy : '—', ''),
    build('delivery_scheduled', 'Delivery Scheduled', scheduledDelivery ? scheduledDelivery.date : null, null, !!deliveredDelivery, !!scheduledDelivery, scheduledDelivery ? scheduledDelivery.receiverName : '—', scheduledDelivery ? scheduledDelivery.deliveryNumber : ''),
    build('delivered', 'Delivered', null, deliveredDelivery ? deliveredDelivery.date : null, !!deliveredDelivery, false, deliveredDelivery ? deliveredDelivery.receiverName : '—', ''),
    build('pod', 'POD', null, null, !!(deliveredDelivery && (deliveredDelivery.clientSignatureUrl || deliveredDelivery.slipFile)), false, deliveredDelivery ? deliveredDelivery.receiverName : '—', '',
      deliveredDelivery && deliveredDelivery.clientSignatureUrl ? [{ name: 'Signed POD', url: deliveredDelivery.clientSignatureUrl }] : []),
  ];
}
const JOURNEY_STATUS_COLOR = { 'Complete': scheduleColor('Complete'), 'In Progress': scheduleColor('In Progress'), 'Delayed': scheduleColor('Delayed'), 'Not Started': scheduleColor('Not Started') };
function MaterialJourneyTab({ ctx, project }) {
  const [detailFor, setDetailFor] = useState(null);
  return (
    <div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">Derived live from Procurement, Production, Export, Warehouse, and Delivery — click any stage for its detail. A red stage is past its tracked planned date with no actual date on file yet.</p>
      {project.scopes.length === 0 ? <EmptyState text="No scopes yet." /> : project.scopes.map(scope => {
        const journey = deriveScopeJourney(ctx, project, scope);
        return (
          <Collapsible key={scope.id} title={scope.name} count={`${journey.filter(s => s.status === 'Complete').length}/${journey.length}`}>
            <div className="overflow-x-auto pb-2">
              <div className="flex items-stretch gap-0 min-w-max">
                {journey.map((s, i) => (
                  <div key={s.key} className="flex items-center">
                    <button onClick={() => setDetailFor({ ...s, scopeName: scope.name })} className="flex flex-col items-center w-24 shrink-0 text-center group">
                      <span className="w-4 h-4 rounded-full border-2 border-white shadow group-hover:scale-110 transition-transform" style={{ background: JOURNEY_STATUS_COLOR[s.status] }} />
                      <span className="text-[10px] font-semibold mt-1 leading-tight">{s.label}</span>
                      <span className="text-[9px] text-[var(--leon-black)]/40 mt-0.5">{s.actual ? fmtDate(s.actual) : s.planned ? fmtDate(s.planned) : '—'}</span>
                    </button>
                    {i < journey.length - 1 && <div className="h-0.5 w-6 shrink-0 mt-2" style={{ background: JOURNEY_STATUS_COLOR[journey[i + 1].status] !== JOURNEY_STATUS_COLOR['Not Started'] || s.status === 'Complete' ? '#c9beac' : '#e5ddd0' }} />}
                  </div>
                ))}
              </div>
            </div>
          </Collapsible>
        );
      })}
      <RecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} title={detailFor ? `${detailFor.scopeName} — ${detailFor.label}` : ''} printable
        fields={detailFor ? [
          { label: 'Status', value: detailFor.status }, { label: 'Planned Date', value: fmtDate(detailFor.planned) },
          { label: 'Actual Date', value: fmtDate(detailFor.actual) }, { label: 'Responsible', value: detailFor.responsible },
          { label: 'Notes', value: detailFor.notes || '—' },
        ] : []}
        attachments={detailFor ? detailFor.attachments : []}
      />
    </div>
  );
}

// ---- Export (11-step workflow + freight estimates -> Freight PO) ----------
const EXPORT_SUBTABS = [
  { key: 'shipping', label: 'Shipping Info', icon: '🚢' },
  { key: 'freight', label: 'Freight Estimate', icon: '💵' },
];
function ExportTab({ ctx, project }) {
  const [sub, setSub] = useState('shipping');
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)]">
        {EXPORT_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />
      {sub === 'shipping' && <ShippingInfoSubTab ctx={ctx} project={project} />}
      {sub === 'freight' && <FreightEstimateSubTab ctx={ctx} project={project} />}
    </div>
  );
}
// The new primary workflow: one entity per physical container, each with
// its own copy of the 11-step document checklist and a completeness
// warning. The older per-scope document log (below it, unchanged) stays in
// place rather than being migrated, so nothing already on file disappears.
// Containers now live INSIDE each scope's own section, alongside that
// scope's 11-step document log — "see per container shipping for each
// scope" — instead of a separate flat container list disconnected from the
// document workflow. A container covering more than one scope appears under
// each of them; one with no scope yet falls into "Unassigned Containers".
function ShippingInfoSubTab({ ctx, project }) {
  const editable = ctx.canEdit('export');
  const [addContainerFor, setAddContainerFor] = useState(null);
  const [docDetailFor, setDocDetailFor] = useState(null);
  const [claimFor, setClaimFor] = useState(null);
  const [editContainerFor, setEditContainerFor] = useState(null);
  const unassignedContainers = containersForProject(ctx.exportContainers, project.id).filter(c => containerScopeIdsForProject(c, project.id).length === 0);

  function ScopeShippingBlock({ scope }) {
    const docs = project.exportDocuments.filter(d => d.scopeId === scope.id);
    const containers = containersForProjectScope(ctx.exportContainers, project.id, scope.id);
    return (
      <Collapsible title={scope.name} count={containers.length + docs.length}>
        <div className="flex items-center justify-between mb-1.5">
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50">Containers</p>
          {editable && <button onClick={() => setAddContainerFor(scope)} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Container</button>}
        </div>
        {containers.length === 0 ? <p className="text-xs text-[var(--leon-black)]/40 italic mb-3">No containers for this scope yet.</p> : (
          <div className="space-y-3 mb-3">
            {containers.map(c => <ExportContainerCard key={c.id} ctx={ctx} project={project} container={c} editable={editable} onLogClaim={() => setClaimFor(c)} onEdit={() => setEditContainerFor(c)} />)}
          </div>
        )}

        {/* The per-scope Document Log was removed: export paperwork is captured
            on the container itself when it is created (its own Documents
            checklist), so this repeated the same records in a second place
            where they could drift out of step. */}
      </Collapsible>
    );
  }

  const overview = (
    <div>
      {project.scopes.length === 0 ? (
        <EmptyState text="No scopes yet — the export workflow is tracked per scope." />
      ) : project.scopes.map(scope => <ScopeShippingBlock key={scope.id} scope={scope} />)}

      {(unassignedContainers.length > 0 || editable) && (
        <Collapsible title="Unassigned Containers" count={unassignedContainers.length}>
          <div className="flex justify-end mb-2">{editable && <Button size="sm" variant="ghost" onClick={() => setAddContainerFor({ id: null })}>+ Add Container</Button>}</div>
          {unassignedContainers.length === 0 ? <EmptyState text="No containers without a scope." /> : (
            <div className="space-y-3">
              {unassignedContainers.map(c => <ExportContainerCard key={c.id} ctx={ctx} project={project} container={c} editable={editable} onLogClaim={() => setClaimFor(c)} onEdit={() => setEditContainerFor(c)} />)}
            </div>
          )}
        </Collapsible>
      )}
    </div>
  );

  return (
    <div>
      <ScopeHubTabs ctx={ctx} project={project} overview={overview} renderScope={scope => <ScopeShippingBlock scope={scope} />} />

      <AddExportContainerModal open={!!addContainerFor} defaultScopeId={addContainerFor?.id} onClose={() => setAddContainerFor(null)} ctx={ctx} project={project} />
      <AddExportContainerModal open={!!editContainerFor} editContainer={editContainerFor} onClose={() => setEditContainerFor(null)} ctx={ctx} project={project} />
      <RecordDetailModal open={!!docDetailFor} onClose={() => setDocDetailFor(null)} title={`Export Document — ${docDetailFor?.name || ''}`} printable
        fields={docDetailFor ? [
          { label: 'Export Step', value: docDetailFor.stepName }, { label: 'Scope', value: docDetailFor.scopeName },
          { label: 'Date', value: fmtDate(docDetailFor.date) }, { label: 'Tracking Number', value: docDetailFor.trackingNumber },
          { label: 'Container Number', value: docDetailFor.containerNumber }, { label: 'Notes', value: docDetailFor.note },
        ] : []}
        attachments={docDetailFor && docDetailFor.fileUrl ? [{ name: docDetailFor.file, url: docDetailFor.fileUrl }] : []}
      />
      <AddLogisticsClaimModal open={!!claimFor} onClose={() => setClaimFor(null)} ctx={ctx} project={project} container={claimFor} />
    </div>
  );
}
function ExportContainerCard({ ctx, project, container, editable, onLogClaim, onEdit }) {
  const missing = EXPORT_DOC_CHECKLIST_STEPS.filter(s => !docStepSatisfied(container.documents[s.key]));
  const freight = ctx.freightForwarders.find(f => f.id === container.freightCompanyId);
  const broker = ctx.freightForwarders.find(f => f.id === container.brokerCompanyId);
  const scopeIdsForProject = containerScopeIdsForProject(container, project.id);
  const scopeNames = scopeIdsForProject.map(id => (project.scopes.find(s => s.id === id) || {}).name).filter(Boolean);
  const [showReceive, setShowReceive] = useState(false);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap mb-1.5">
        <div className="flex items-center gap-1.5 flex-wrap">
          <Badge tone="black">{container.containerNumber}</Badge>
          {editable ? (
            <Select value={container.status} onChange={e => ctx.updateExportContainer(container.id, { status: e.target.value })} className="!w-32 !py-0.5 !text-xs">
              {EXPORT_CONTAINER_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          ) : <StatusBadge status={container.status} />}
          {scopeNames.map(n => <Badge key={n} tone="neutral">{n}</Badge>)}
        </div>
        <div className="flex items-center gap-1.5">
          <span title={container.handoffDate ? `Handed off ${fmtDate(container.handoffDate)} by ${container.handoffBy}` : ''}><Badge tone={container.responsibleParty === 'Logistics' ? 'green' : 'neutral'}>Responsible: {container.responsibleParty}</Badge></span>
          {missing.length > 0 ? (
            <Badge tone="red">⚠ {missing.length} document{missing.length === 1 ? '' : 's'} missing</Badge>
          ) : (
            <Badge tone="green">All documents on file</Badge>
          )}
        </div>
      </div>
      <div className="grid sm:grid-cols-3 gap-x-4 gap-y-1 text-xs text-[var(--leon-black)]/60 mb-2">
        <p>BL: <strong className="text-[var(--leon-black)]">{container.blNumber || '—'}</strong></p>
        <p>Freight Co: <strong className="text-[var(--leon-black)]">{freight ? freight.name : '—'}</strong></p>
        <p>Broker: <strong className="text-[var(--leon-black)]">{broker ? broker.name : '—'}</strong></p>
        <p>Inland Freight: <strong className="text-[var(--leon-black)]">{container.inlandFreightCompany || '—'}</strong></p>
        <p>From Port: <strong className="text-[var(--leon-black)]">{container.fromPort || '—'}</strong></p>
        <p>To Port: <strong className="text-[var(--leon-black)]">{container.toPort || '—'}</strong></p>
        <p>ETL: <strong className="text-[var(--leon-black)]">{container.etl ? fmtDate(container.etl) : '—'}</strong></p>
        <p>ETD: <strong className="text-[var(--leon-black)]">{container.etd ? fmtDate(container.etd) : '—'}</strong></p>
        <p>ETA: <strong className="text-[var(--leon-black)]">{container.eta ? fmtDate(container.eta) : '—'}</strong></p>
        <p>Tracking: {container.trackingLink ? <a href={container.trackingLink} target="_blank" rel="noopener noreferrer" className="text-[var(--leon-brown)] font-semibold hover:underline">Track Shipment ↗</a> : <strong className="text-[var(--leon-black)]">—</strong>}</p>
        <p className="sm:col-span-2">Assigned To: <strong className="text-[var(--leon-black)]">{container.assigneeIds && container.assigneeIds.length ? container.assigneeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') : 'Unassigned'}</strong></p>
      </div>
      <div className="flex items-center gap-1.5 flex-wrap mb-2">
        {container.shipmentType && <Badge tone="neutral">{container.shipmentType}</Badge>}
        {container.containerType && <Badge tone="neutral">{container.containerType}</Badge>}
        {container.countryOfOrigin && <Badge tone="neutral">Origin: {container.countryOfOrigin}</Badge>}
        {container.carrier && <Badge tone="neutral">Carrier: {container.carrier}</Badge>}
        {container.bookingNumber && <Badge tone="neutral">Booking: {container.bookingNumber}</Badge>}
        <RiskBadge status={containerRiskStatus(container)} />
        <div className="ml-auto flex items-center gap-3">
          {editable && container.responsibleParty === 'Export' && missing.length === 0 && ['In Transit', 'Arrived', 'Delivered'].includes(container.status) && (
            <button onClick={() => ctx.handoffContainerToLogistics(container.id)} className="text-xs text-[var(--leon-brown)] font-semibold">🤝 Hand Off to Logistics</button>
          )}
          {ctx.canControlInventory && container.responsibleParty === 'Logistics' && container.status === 'Arrived' && container.materialLines.length > 0 && (
            <button onClick={() => setShowReceive(true)} className="text-xs text-[var(--leon-brown)] font-semibold">📦 Receive at Warehouse</button>
          )}
          {editable && onEdit && <button onClick={onEdit} className="text-xs text-[var(--leon-brown)] font-semibold">✎ Edit</button>}
          {editable && onLogClaim && <button onClick={onLogClaim} className="text-xs text-[var(--leon-brown)] font-semibold">⚠ Log Claim</button>}
        </div>
      </div>
      <ReceiveContainerModal open={showReceive} onClose={() => setShowReceive(false)} ctx={ctx} project={project} container={container} />
      <Collapsible title={`Documents (${EXPORT_DOC_CHECKLIST_STEPS.length} Steps)`} count={EXPORT_DOC_CHECKLIST_STEPS.length - missing.length}>
        <div className="space-y-1">
          {EXPORT_DOC_CHECKLIST_STEPS.map(step => {
            const doc = container.documents[step.key] || {};
            return (
              <div key={step.key} className="flex items-center gap-3 py-1 border-b border-[var(--leon-line)] last:border-0">
                <span className="w-5 h-5 rounded-full bg-[var(--leon-cream)] text-[var(--leon-brown)] text-[10px] font-bold flex items-center justify-center shrink-0">{step.order}</span>
                <span className="text-xs flex-1 min-w-0">{step.name}</span>
                {doc.notRequired ? (
                  <Badge tone="neutral">Not Required</Badge>
                ) : (
                  <FileField name={doc.file} url={doc.fileUrl} onChange={(fname, url) => ctx.setContainerDocument(container.id, step.key, { file: fname, fileUrl: url, date: todayISO() })} editable={editable} placeholder="Not yet on file" />
                )}
                {editable && (
                  <label className="flex items-center gap-1 text-[11px] text-[var(--leon-black)]/50 whitespace-nowrap shrink-0">
                    <input type="checkbox" checked={!!doc.notRequired} onChange={e => ctx.setContainerDocument(container.id, step.key, { notRequired: e.target.checked })} /> N/A
                  </label>
                )}
              </div>
            );
          })}
        </div>
      </Collapsible>
    </div>
  );
}
// Manual confirmation gate between "container arrived" and "material is
// allocated" (§ warehouse arrival request) — pre-fills expected quantity
// from the container's own material lines, matching the existing Receiving
// Report's short/over/damaged convention, and only creates an Allocation
// for what's actually confirmed here.
function ReceiveContainerModal({ open, onClose, ctx, project, container }) {
  const [warehouseId, setWarehouseId] = useState('');
  const [lines, setLines] = useState([]);
  const [customsStatus, setCustomsStatus] = useState('Not Started');
  const [actualDeparture, setActualDeparture] = useState('');
  const [actualArrival, setActualArrival] = useState('');
  const [customsEntryDoc, setCustomsEntryDoc] = useState({ file: '', fileUrl: null });
  const [deliveryPodDoc, setDeliveryPodDoc] = useState({ file: '', fileUrl: null });
  useEffect(() => {
    if (!open) return;
    setWarehouseId(ctx.warehouses[0] ? ctx.warehouses[0].id : '');
    setLines(container.materialLines.map(l => {
      const match = ctx.warehouseMaterials.find(m => m.active && m.name.trim().toLowerCase() === l.description.trim().toLowerCase());
      return { containerLineId: l.id, warehouseMaterialId: match ? match.id : '', receivedQuantity: l.quantity, shortQuantity: 0, overQuantity: 0, damagedQuantity: 0, notes: '', photos: [] };
    }));
    setCustomsStatus(container.customsStatus || 'Not Started');
    setActualDeparture(container.actualDeparture || container.etd || '');
    setActualArrival(container.actualArrival || container.eta || todayISO());
    const existingCustomsDoc = container.documents.customs_entry || {};
    const existingPodDoc = container.documents.delivery_pod || {};
    setCustomsEntryDoc({ file: existingCustomsDoc.file || '', fileUrl: existingCustomsDoc.fileUrl || null });
    setDeliveryPodDoc({ file: existingPodDoc.file || '', fileUrl: existingPodDoc.fileUrl || null });
  }, [open, container]);
  function updateLine(containerLineId, fields) { setLines(prev => prev.map(l => l.containerLineId === containerLineId ? { ...l, ...fields } : l)); }
  function submit() {
    if (!warehouseId) return;
    ctx.updateExportContainer(container.id, { customsStatus, actualDeparture: actualDeparture || null, actualArrival: actualArrival || null });
    if (customsEntryDoc.fileUrl) ctx.setContainerDocument(container.id, 'customs_entry', { ...customsEntryDoc, date: todayISO() });
    if (deliveryPodDoc.fileUrl) ctx.setContainerDocument(container.id, 'delivery_pod', { ...deliveryPodDoc, date: todayISO() });
    ctx.receiveContainerMaterials(project.id, container.id, warehouseId, lines);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Receive at Warehouse — ${container.containerNumber}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!warehouseId}>Confirm Receipt &amp; Allocate</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Confirm what actually arrived — only the quantity confirmed here becomes allocated material for the project. A material with no existing warehouse record is created automatically.</p>
        <Field label="Warehouse"><Select value={warehouseId} onChange={e => setWarehouseId(e.target.value)}>{ctx.warehouses.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}</Select></Field>
        <div className="grid grid-cols-3 gap-3 border border-[var(--leon-line)] rounded-lg p-2.5">
          <Field label="Customs Status">
            <Select value={customsStatus} onChange={e => setCustomsStatus(e.target.value)} className="!py-1 !text-xs">
              {CUSTOMS_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
          <Field label="Actual Departure"><TextInput type="date" value={actualDeparture} onChange={e => setActualDeparture(e.target.value)} className="!py-1 !text-xs" /></Field>
          <Field label="Actual Arrival"><TextInput type="date" value={actualArrival} onChange={e => setActualArrival(e.target.value)} className="!py-1 !text-xs" /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3 border border-[var(--leon-line)] rounded-lg p-2.5">
          <Field label="Customs Entry / Duty Documents" hint="Step 10 — usually only finalized once received">
            <FileField name={customsEntryDoc.file} url={customsEntryDoc.fileUrl} onChange={(fname, url) => setCustomsEntryDoc({ file: fname, fileUrl: url })} editable placeholder="Not yet on file" />
          </Field>
          <Field label="Delivery / POD" hint="Step 11 — proof of delivery">
            <FileField name={deliveryPodDoc.file} url={deliveryPodDoc.fileUrl} onChange={(fname, url) => setDeliveryPodDoc({ file: fname, fileUrl: url })} editable placeholder="Not yet on file" />
          </Field>
        </div>
        <div className="space-y-3">
          {groupLinesByProjectScope(container.materialLines, ctx.projects).map(pg => (
            <div key={pg.key}>
              <div className="flex items-center gap-2 mb-1.5 pb-1 border-b-2 border-[var(--leon-brown)]/30">
                <span className="text-sm font-bold">{pg.name}</span>
                {pg.number && <span className="text-[10px] font-mono text-[var(--leon-black)]/40">{pg.number}</span>}
              </div>
              {pg.scopes.map(sg => (
                <div key={sg.key} className="mb-2">
                  <p className="text-[10px] font-bold uppercase tracking-wide text-[var(--leon-brown)] mb-1">{sg.name}</p>
                  <div className="space-y-2">
                  {sg.lines.map(cl => {
                    const line = lines.find(l => l.containerLineId === cl.id) || {};
                    return (
              <div key={cl.id} className="border border-[var(--leon-line)] rounded-lg p-2.5">
                <p className="text-sm font-semibold mb-1.5">{cl.description} <span className="font-normal text-[var(--leon-black)]/50">— Expected {cl.quantity} {cl.unit}</span></p>
                <div className="grid grid-cols-5 gap-2">
                  <Field label="Received"><TextInput type="number" value={line.receivedQuantity} onChange={e => updateLine(cl.id, { receivedQuantity: Number(e.target.value) || 0 })} className="!py-1 !text-xs" /></Field>
                  <Field label="Short"><TextInput type="number" value={line.shortQuantity} onChange={e => updateLine(cl.id, { shortQuantity: Number(e.target.value) || 0 })} className="!py-1 !text-xs" /></Field>
                  <Field label="Over"><TextInput type="number" value={line.overQuantity} onChange={e => updateLine(cl.id, { overQuantity: Number(e.target.value) || 0 })} className="!py-1 !text-xs" /></Field>
                  <Field label="Damaged"><TextInput type="number" value={line.damagedQuantity} onChange={e => updateLine(cl.id, { damagedQuantity: Number(e.target.value) || 0 })} className="!py-1 !text-xs" /></Field>
                  <Field label="Warehouse Material">
                    <Select value={line.warehouseMaterialId} onChange={e => updateLine(cl.id, { warehouseMaterialId: e.target.value })} className="!py-1 !text-xs">
                      <option value="">— create new —</option>
                      {ctx.warehouseMaterials.filter(m => m.active).map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                    </Select>
                  </Field>
                </div>
                <div className="mt-2">
                  <p className="text-xs font-semibold mb-1">Inspection Photos</p>
                  <div className="flex items-center gap-2 flex-wrap">
                    {(line.photos || []).map((p, i) => (
                      <span key={i} className="relative">
                        <Photo src={p} title="Inspection photo" className="w-12 h-12 object-cover rounded-md border border-[var(--leon-line)]" />
                        <button type="button" onClick={() => updateLine(cl.id, { photos: line.photos.filter((_, idx) => idx !== i) })} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
                      </span>
                    ))}
                    <ImagePicker url={null} onChange={url => updateLine(cl.id, { photos: [...(line.photos || []), url] })} size={48} />
                  </div>
                </div>
              </div>
                    );
                  })}
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
      </div>
    </Modal>
  );
}
// A container can carry material for several projects at once (§ multi-
// project containers) — `form.shipments` holds one row per project, each
// with its own scope selection, instead of the single project + scopeIds
// this form used to collect. `project` is still the project this modal was
// opened FROM (used as the default first row and to pick a sensible default
// scope), but any number of other projects can be added alongside it.
function AddExportContainerModal({ open, defaultScopeId, onClose, ctx, project, editContainer }) {
  const isEdit = !!editContainer;
  const blank = {
    shipments: [{ projectId: project.id, scopeIds: [] }],
    blNumber: '', freightCompanyId: '', brokerCompanyId: '', inlandFreightCompany: '', etl: '', etd: '', eta: '', fromPort: '', toPort: '', assigneeIds: [], notes: '', shipmentType: 'Ocean FCL', containerType: '', countryOfOrigin: '', carrier: '', bookingNumber: '', trackingLink: '', materialLines: [],
  };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (editContainer) {
      setForm({
        shipments: editContainer.shipments && editContainer.shipments.length ? editContainer.shipments.map(s => ({ projectId: s.projectId, scopeIds: s.scopeIds || [] })) : [{ projectId: project.id, scopeIds: [] }],
        blNumber: editContainer.blNumber, freightCompanyId: editContainer.freightCompanyId || '', brokerCompanyId: editContainer.brokerCompanyId || '',
        inlandFreightCompany: editContainer.inlandFreightCompany, etl: editContainer.etl || '', etd: editContainer.etd || '', eta: editContainer.eta || '',
        fromPort: editContainer.fromPort, toPort: editContainer.toPort, assigneeIds: editContainer.assigneeIds || [], notes: editContainer.notes,
        shipmentType: editContainer.shipmentType, containerType: editContainer.containerType, countryOfOrigin: editContainer.countryOfOrigin,
        carrier: editContainer.carrier, bookingNumber: editContainer.bookingNumber, trackingLink: editContainer.trackingLink || '', materialLines: editContainer.materialLines || [],
      });
    } else {
      setForm({ ...blank, shipments: [{ projectId: project.id, scopeIds: defaultScopeId ? [defaultScopeId] : [] }] });
    }
  }, [open, defaultScopeId, editContainer, project]);
  // Every PI (of any status) tied to a scope ANY shipment row carries —
  // materials are only ever picked from these lines, never typed fresh, so
  // a container's contents can never drift from what was actually ordered.
  // Spans every project in form.shipments, not just the one this modal was
  // opened from.
  const availablePiLines = form.shipments.flatMap(sh => {
    const shProject = ctx.projects.find(p => p.id === sh.projectId);
    if (!shProject) return [];
    return (shProject.proformaInvoices || [])
      .filter(pi => sh.scopeIds.includes(pi.scopeId))
      .flatMap(pi => pi.materialLines.map(line => ({
        pi, line, projectName: shProject.name, projectId: shProject.id, scopeId: pi.scopeId,
        scopeName: ((shProject.scopes || []).find(s => s.id === pi.scopeId) || {}).name || 'No scope',
      })));
  });
  function addMaterialLine(pi, line, projectId, scopeId) {
    setForm(f => ({ ...f, materialLines: [...f.materialLines, makeContainerMaterialLine({
      piId: pi.id, piLineId: line.id, materialId: line.materialId,
      projectId, scopeId,
      description: line.description, quantity: line.quantity, unit: line.unit })] }));
  }
  // Selected lines, grouped project -> scope -> items for the summary below.
  const groupedSelected = groupLinesByProjectScope(form.materialLines, ctx.projects);
  function updateMaterialLineQty(id, quantity) { setForm(f => ({ ...f, materialLines: f.materialLines.map(l => l.id === id ? { ...l, quantity: Number(quantity) || 0 } : l) })); }
  function removeMaterialLine(id) { setForm(f => ({ ...f, materialLines: f.materialLines.filter(l => l.id !== id) })); }
  function submit() {
    const shipments = form.shipments.filter(sh => sh.projectId);
    if (!shipments.length) return;
    const payload = {
      ...form, shipments,
      freightCompanyId: form.freightCompanyId || null, brokerCompanyId: form.brokerCompanyId || null,
      etl: form.etl || null, etd: form.etd || null, eta: form.eta || null,
    };
    if (isEdit) ctx.updateExportContainer(editContainer.id, payload);
    else ctx.addExportContainer(payload);
    onClose();
  }
  function setShipmentProject(idx, projectId) {
    setForm(f => ({ ...f, shipments: f.shipments.map((sh, i) => (i === idx ? { projectId, scopeIds: [] } : sh)) }));
  }
  function toggleShipmentScope(idx, scopeId) {
    setForm(f => ({ ...f, shipments: f.shipments.map((sh, i) => (i === idx ? { ...sh, scopeIds: sh.scopeIds.includes(scopeId) ? sh.scopeIds.filter(x => x !== scopeId) : [...sh.scopeIds, scopeId] } : sh)) }));
  }
  function addShipmentRow() { setForm(f => ({ ...f, shipments: [...f.shipments, { projectId: '', scopeIds: [] }] })); }
  function removeShipmentRow(idx) { setForm(f => ({ ...f, shipments: f.shipments.filter((_, i) => i !== idx) })); }
  function toggleAssignee(id) { setForm(f => ({ ...f, assigneeIds: f.assigneeIds.includes(id) ? f.assigneeIds.filter(x => x !== id) : [...f.assigneeIds, id] })); }
  const usedProjectIds = form.shipments.map(sh => sh.projectId);
  const exportManagers = ctx.teamDirectory.filter(p => p.active && p.securityRole === 'Export Manager');
  const assigneeOptions = exportManagers.length > 0 ? exportManagers : ctx.teamDirectory.filter(p => p.active);
  return (
    <Modal open={open} onClose={onClose} wide title={isEdit ? `Edit Container ${editContainer.containerNumber}` : 'Add Export Container'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Container'}</Button></>}>
      <div className="space-y-3">
        {!isEdit && <p className="text-xs text-[var(--leon-black)]/50">A LEON Export number is generated automatically.</p>}
        <Field label="Projects &amp; Scopes in this Container" hint="A container can carry material for more than one project — add a row per project.">
          <div className="space-y-2 border border-[var(--leon-line)] rounded-lg p-2">
            {form.shipments.map((sh, idx) => {
              const shProject = ctx.projects.find(p => p.id === sh.projectId);
              return (
                <div key={idx} className="border border-[var(--leon-line)] rounded-lg p-2">
                  <div className="flex items-center gap-2 mb-1.5">
                    <Select value={sh.projectId} onChange={e => setShipmentProject(idx, e.target.value)} className="!py-1 !text-xs flex-1">
                      <option value="">— select project —</option>
                      {ctx.projects.filter(p => p.id === sh.projectId || !usedProjectIds.includes(p.id)).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                    </Select>
                    {form.shipments.length > 1 && <IconBtn title="Remove project" onClick={() => removeShipmentRow(idx)}>✕</IconBtn>}
                  </div>
                  {shProject && (
                    <div className="flex flex-wrap gap-1.5">
                      {shProject.scopes.length === 0 ? (
                        <span className="text-xs text-[var(--leon-black)]/40">No scopes yet.</span>
                      ) : shProject.scopes.map(s => (
                        <label key={s.id} className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border cursor-pointer ${sh.scopeIds.includes(s.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
                          <input type="checkbox" className="hidden" checked={sh.scopeIds.includes(s.id)} onChange={() => toggleShipmentScope(idx, s.id)} /> {s.name}
                        </label>
                      ))}
                    </div>
                  )}
                </div>
              );
            })}
            <button type="button" onClick={addShipmentRow} className="text-xs text-[var(--leon-brown)] font-semibold">+ Add Another Project</button>
          </div>
        </Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Shipment Type">
            <Select value={form.shipmentType} onChange={e => setForm({ ...form, shipmentType: e.target.value })}>
              {SHIPMENT_TYPES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
          <Field label="Container Type">
            <Select value={form.containerType} onChange={e => setForm({ ...form, containerType: e.target.value })}>
              <option value="">— none —</option>
              {CONTAINER_TYPES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
          <Field label="Country of Origin"><TextInput value={form.countryOfOrigin} onChange={e => setForm({ ...form, countryOfOrigin: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="BL Number"><TextInput value={form.blNumber} onChange={e => setForm({ ...form, blNumber: e.target.value })} /></Field>
          <Field label="Booking Number"><TextInput value={form.bookingNumber} onChange={e => setForm({ ...form, bookingNumber: e.target.value })} /></Field>
          <Field label="Carrier"><TextInput value={form.carrier} onChange={e => setForm({ ...form, carrier: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Inland Freight Company"><TextInput value={form.inlandFreightCompany} onChange={e => setForm({ ...form, inlandFreightCompany: e.target.value })} /></Field>
          <Field label="Container Tracking Link" hint="Carrier's live tracking URL for this shipment"><TextInput value={form.trackingLink} onChange={e => setForm({ ...form, trackingLink: e.target.value })} placeholder="https://…" /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Freight Company" hint="From the vendor freight list">
            <Select value={form.freightCompanyId} onChange={e => setForm({ ...form, freightCompanyId: e.target.value })}>
              <option value="">— none —</option>
              {ctx.freightForwarders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
            </Select>
          </Field>
          <Field label="Broker Company" hint="From the vendor freight list">
            <Select value={form.brokerCompanyId} onChange={e => setForm({ ...form, brokerCompanyId: e.target.value })}>
              <option value="">— none —</option>
              {ctx.freightForwarders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="From Port"><TextInput value={form.fromPort} onChange={e => setForm({ ...form, fromPort: e.target.value })} /></Field>
          <Field label="To Port"><TextInput value={form.toPort} onChange={e => setForm({ ...form, toPort: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Est. Time of Loading"><TextInput type="date" value={form.etl} onChange={e => setForm({ ...form, etl: e.target.value })} /></Field>
          <Field label="Est. Time of Departure"><TextInput type="date" value={form.etd} onChange={e => setForm({ ...form, etd: e.target.value })} /></Field>
          <Field label="Est. Time of Arrival"><TextInput type="date" value={form.eta} onChange={e => setForm({ ...form, eta: e.target.value })} /></Field>
        </div>
        <Field label="Assigned To" hint="Multiple people can be assigned to one container.">
          <div className="flex flex-wrap gap-1.5 border border-[var(--leon-line)] rounded-lg p-2">
            {assigneeOptions.length === 0 ? <span className="text-xs text-[var(--leon-black)]/40">No team members available.</span> : assigneeOptions.map(p => (
              <label key={p.id} className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border cursor-pointer ${form.assigneeIds.includes(p.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
                <input type="checkbox" className="hidden" checked={form.assigneeIds.includes(p.id)} onChange={() => toggleAssignee(p.id)} /> {p.name}
              </label>
            ))}
          </div>
        </Field>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>

        <div className="pt-2 border-t border-[var(--leon-line)]">
          <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">Materials in this Container</p>
          {form.materialLines.length > 0 && (
            <div className="space-y-2 mb-3">
              {groupedSelected.map(pg => (
                <div key={pg.key} className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
                  <div className="bg-[var(--leon-cream)] px-2.5 py-1.5 flex items-center gap-2">
                    <span className="text-xs font-bold">{pg.name}</span>
                    {pg.number && <span className="text-[10px] text-[var(--leon-black)]/40 font-mono">{pg.number}</span>}
                    <span className="text-[10px] text-[var(--leon-black)]/40 ml-auto">
                      {pg.scopes.reduce((n, sg) => n + sg.lines.length, 0)} item{pg.scopes.reduce((n, sg) => n + sg.lines.length, 0) === 1 ? '' : 's'}
                    </span>
                  </div>
                  {pg.scopes.map(sg => (
                    <div key={sg.key}>
                      <div className="px-2.5 py-1 bg-white border-t border-[var(--leon-line)] text-[10px] font-bold uppercase tracking-wide text-[var(--leon-brown)]">{sg.name}</div>
                      {sg.lines.map(l => (
                        <div key={l.id} className="flex items-center gap-2 text-xs px-2.5 py-1 border-t border-[var(--leon-line)]">
                          <span className="flex-1 min-w-0 truncate" title={l.description}>{l.description}</span>
                          <TextInput type="number" value={l.quantity} onChange={e => updateMaterialLineQty(l.id, e.target.value)} className="!w-20 !py-0.5 !text-xs" />
                          <span className="text-[var(--leon-black)]/40">{l.unit}</span>
                          <IconBtn title="Remove" onClick={() => removeMaterialLine(l.id)}>✕</IconBtn>
                        </div>
                      ))}
                    </div>
                  ))}
                </div>
              ))}
            </div>
          )}
          {form.shipments.every(sh => sh.scopeIds.length === 0) ? (
            <p className="text-xs text-[var(--leon-black)]/40">Select at least one scope above to see its PI material lines.</p>
          ) : availablePiLines.length === 0 ? (
            <p className="text-xs text-[var(--leon-black)]/40">No PI material lines yet for the selected scope(s) — add a material list to a PI first.</p>
          ) : (
            <div className="space-y-1">
              {/* Same project -> scope grouping as the selected list, so both
                  halves of the picker read the same way. */}
              {Object.entries(availablePiLines.reduce((acc, row) => {
                const k = row.projectName + '\u0000' + row.scopeName;
                (acc[k] = acc[k] || []).push(row); return acc;
              }, {})).map(([k, rows]) => {
                const [projectName, scopeName] = k.split('\u0000');
                const added = new Set(form.materialLines.map(l => l.piLineId));
                return (
                  <div key={k} className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
                    <div className="bg-[var(--leon-cream)] px-2.5 py-1 flex items-center gap-2">
                      <span className="text-[11px] font-bold">{projectName}</span>
                      <span className="text-[10px] text-[var(--leon-brown)] font-semibold uppercase tracking-wide">{scopeName}</span>
                      <button
                        onClick={() => rows.filter(r => !added.has(r.line.id)).forEach(r => addMaterialLine(r.pi, r.line, r.projectId, r.scopeId))}
                        className="ml-auto text-[10px] text-[var(--leon-brown)] font-semibold">+ Add all</button>
                    </div>
                    {rows.map(({ pi, line, projectId, scopeId }) => (
                      <div key={line.id} className="flex items-center gap-2 text-xs px-2.5 py-1 border-t border-[var(--leon-line)]">
                        <span className="text-[var(--leon-black)]/40 font-mono text-[10px]">{pi.piNumber}</span>
                        <span className="flex-1 min-w-0 truncate" title={line.description}>{line.description} <span className="text-[var(--leon-black)]/40">({line.quantity} {line.unit})</span></span>
                        {added.has(line.id)
                          ? <span className="text-[10px] text-[var(--leon-black)]/35">Added</span>
                          : <button onClick={() => addMaterialLine(pi, line, projectId, scopeId)} className="text-[var(--leon-brown)] font-semibold">+ Add</button>}
                      </div>
                    ))}
                  </div>
                );
              })}
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
}
// Shared capture form for the Shortage/Damage/Discrepancy and Logistics
// Claims reports — one collection (ctx.logisticsClaims), opened either from
// a specific container (Export tab, container/vendor pre-filled) or from
// Warehouse Receiving (project/scope left for the user to pick), matching
// how the two reports are really just different filtered views of the same
// underlying event.
function AddLogisticsClaimModal({ open, onClose, ctx, project, container, defaultVendorId }) {
  const blank = { claimType: 'Shortage', description: '', responsibleParty: '', claimAmount: '', replacementRequired: false, replacementStatus: '', replacementEta: '', vendorId: defaultVendorId || '', projectId: project ? project.id : '', scopeId: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, vendorId: defaultVendorId || '', projectId: project ? project.id : '' }); }, [open, defaultVendorId, project]);
  const pickedProject = project || ctx.projects.find(p => p.id === form.projectId);
  function submit() {
    ctx.addLogisticsClaim({
      ...form,
      projectId: form.projectId || null, scopeId: form.scopeId || null,
      containerId: container ? container.id : null, vendorId: form.vendorId || null,
      claimAmount: Number(form.claimAmount) || 0, replacementEta: form.replacementEta || null,
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={container ? `Log Claim — Container ${container.containerNumber}` : 'Log Logistics Claim'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Log Claim</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Type">
            <Select value={form.claimType} onChange={e => setForm({ ...form, claimType: e.target.value })}>
              {LOGISTICS_CLAIM_TYPES.map(t => <option key={t}>{t}</option>)}
            </Select>
          </Field>
          <Field label="Vendor">
            <Select value={form.vendorId} onChange={e => setForm({ ...form, vendorId: e.target.value })}>
              <option value="">— none —</option>
              {ctx.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
            </Select>
          </Field>
        </div>
        {!project && (
          <Field label="Project">
            <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value, scopeId: '' })}>
              <option value="">— none —</option>
              {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
        )}
        {pickedProject && (
          <Field label="Scope">
            <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
              <option value="">— none —</option>
              {pickedProject.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        )}
        <Field label="Description"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Responsible Party"><TextInput value={form.responsibleParty} onChange={e => setForm({ ...form, responsibleParty: e.target.value })} /></Field>
          <Field label="Claim Amount"><TextInput type="number" value={form.claimAmount} onChange={e => setForm({ ...form, claimAmount: e.target.value })} /></Field>
        </div>
        <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={form.replacementRequired} onChange={e => setForm({ ...form, replacementRequired: e.target.checked })} /> Replacement required</label>
        {form.replacementRequired && (
          <div className="grid grid-cols-2 gap-3">
            <Field label="Replacement Status"><TextInput value={form.replacementStatus} onChange={e => setForm({ ...form, replacementStatus: e.target.value })} placeholder="e.g. Requested, In Production, Shipped, Delivered" /></Field>
            <Field label="Replacement ETA"><TextInput type="date" value={form.replacementEta} onChange={e => setForm({ ...form, replacementEta: e.target.value })} /></Field>
          </div>
        )}
      </div>
    </Modal>
  );
}
function FreightEstimateSubTab({ ctx, project }) {
  const editable = ctx.canEdit('export');
  const [showAddFreight, setShowAddFreight] = useState(false);
  const [revisionFor, setRevisionFor] = useState(null);
  const canExportApprove = canApproveFreightExport(ctx.currentRole);
  const canAdminApprove = canApproveFreightAdmin(ctx.currentRole);
  const [estimateDetailFor, setEstimateDetailFor] = useState(null);
  return (
    <div>
      <Collapsible title="Freight Estimates" count={project.freightEstimates.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowAddFreight(true)}>+ Add Freight Estimate</Button>}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">A freight estimate becomes a Freight PO only once both Export Manager and Admin/Accounting have approved it.</p>
        {project.freightEstimates.length === 0 ? <EmptyState text="No freight estimates yet." /> : (
          <div className="space-y-2">
            {project.freightEstimates.map(fre => {
              const scope = project.scopes.find(s => s.id === fre.scopeId);
              return (
              <div key={fre.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap">
                  <div className="cursor-pointer" onClick={() => setEstimateDetailFor({ ...fre, vendorName: fre.carrier })}>
                    <p className="text-sm font-bold hover:underline"><Badge tone="black">{fre.estimateNumber}</Badge> {fre.carrier} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(fre.amount)} (Rev {fre.revisions?.[fre.revisions.length - 1]?.revision || 1})</span></p>
                    <div className="flex items-center gap-1.5 mt-0.5"><StatusBadge status={fre.status} /><Badge tone="neutral">{fre.category || 'Original Order'}</Badge>{scope && <Badge tone="neutral">{scope.name}</Badge>}</div>
                    <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">{fre.description} · {fmtDate(fre.date)}</p>
                  </div>
                  {fre.poId && <Badge tone="green">Freight PO Issued</Badge>}
                </div>
                <div className="flex items-center gap-4 mt-2 text-xs flex-wrap">
                  <ApprovalPill label="Export Manager" approved={fre.exportApproved} by={fre.exportApprovedBy} date={fre.exportApprovedDate}
                    onApprove={canExportApprove && !fre.exportApproved ? () => ctx.approveFreight(project.id, fre.id, 'export') : null} />
                  <ApprovalPill label="Admin/Accounting" approved={fre.adminApproved} by={fre.adminApprovedBy} date={fre.adminApprovedDate}
                    onApprove={canAdminApprove && !fre.adminApproved ? () => ctx.approveFreight(project.id, fre.id, 'admin') : null} />
                  {editable && <Button size="sm" variant="ghost" onClick={() => setRevisionFor(fre)}>+ Add Revision</Button>}
                </div>
                {fre.revisions && fre.revisions.length > 0 && (
                  <div className="mt-2 pt-2 border-t border-[var(--leon-line)] space-y-1">
                    {fre.revisions.map(r => (
                      <p key={r.id} className="text-[11px] text-[var(--leon-black)]/50 flex items-center gap-1.5 flex-wrap">
                        Rev {r.revision}: {fmtMoney(r.amount)} · {fmtDate(r.date)}
                        <FileField name={r.file} url={r.fileUrl} onChange={(fname, url) => ctx.updateFreightEstimateRevision(project.id, fre.id, r.id, { file: fname, fileUrl: url })} editable={editable} />
                        {r.note ? ` — ${r.note}` : ''}
                      </p>
                    ))}
                  </div>
                )}
              </div>
              );
            })}
          </div>
        )}
      </Collapsible>

      {project.freightPOs.length > 0 && (
        <Collapsible title="Freight POs" count={project.freightPOs.length}>
          <div className="space-y-2">
            {project.freightPOs.map(po => <PoCard key={po.id} ctx={ctx} project={project} po={po} partyType="Freight" editable={editable} />)}
          </div>
        </Collapsible>
      )}

      <AddFreightEstimateModal open={showAddFreight} onClose={() => setShowAddFreight(false)} ctx={ctx} project={project} />
      <AddFreightEstimateRevisionModal open={!!revisionFor} fre={revisionFor} onClose={() => setRevisionFor(null)} ctx={ctx} project={project} />
      <EstimateDetailModal open={!!estimateDetailFor} estimate={estimateDetailFor} partyType="Freight" onClose={() => setEstimateDetailFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function AddExportDocumentModal({ open, scope, onClose, ctx, project }) {
  const [form, setForm] = useState({ step: EXPORT_WORKFLOW_STEPS[0].key, name: '', file: '', fileUrl: null, date: todayISO(), trackingNumber: '', containerNumber: '', note: '' });
  useEffect(() => { if (open) setForm({ step: EXPORT_WORKFLOW_STEPS[0].key, name: '', file: '', fileUrl: null, date: todayISO(), trackingNumber: '', containerNumber: '', note: '' }); }, [open]);
  if (!scope) return null;
  function submit() { if (!form.name.trim()) return; ctx.addExportDocument(project.id, { ...form, scopeId: scope.id }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Add Export Document — ${scope.name}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add</Button></>}>
      <div className="space-y-3">
        <Field label="Workflow Step">
          <Select value={form.step} onChange={e => setForm({ ...form, step: e.target.value })}>
            {EXPORT_WORKFLOW_STEPS.map(s => <option key={s.key} value={s.key}>{s.order} – {s.name}</option>)}
          </Select>
        </Field>
        <Field label="Document Name"><TextInput value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Tracking Number (optional)"><TextInput value={form.trackingNumber} onChange={e => setForm({ ...form, trackingNumber: e.target.value })} /></Field>
          <Field label="Container Number (optional)"><TextInput value={form.containerNumber} onChange={e => setForm({ ...form, containerNumber: e.target.value })} /></Field>
        </div>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function AddFreightEstimateModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ scopeId: '', forwarderId: '', category: VENDOR_ESTIMATE_CATEGORIES[0], description: '', amount: '', date: todayISO(), file: '', fileUrl: null });
  useEffect(() => { if (open) setForm({ scopeId: '', forwarderId: ctx.freightForwarders[0]?.id || '', category: VENDOR_ESTIMATE_CATEGORIES[0], description: '', amount: '', date: todayISO(), file: '', fileUrl: null }); }, [open]);
  function submit() {
    const forwarder = ctx.freightForwarders.find(f => f.id === form.forwarderId);
    if (!forwarder || !form.amount) return;
    ctx.addFreightEstimate(project.id, { scopeId: form.scopeId || null, forwarderId: forwarder.id, carrier: forwarder.name, category: form.category, description: form.description, amount: Number(form.amount), date: form.date, file: form.file, fileUrl: form.fileUrl });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Add Freight Estimate" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Estimate</Button></>}>
      <div className="space-y-3">
        <Field label="Scope (optional)" hint="Leave blank if this shipment covers multiple scopes.">
          <Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>
            <option value="">— combined / project-wide —</option>
            {project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </Select>
        </Field>
        <Field label="Freight Forwarder" hint="Managed under Vendors → Freight Forwarders.">
          <Select value={form.forwarderId} onChange={e => setForm({ ...form, forwarderId: e.target.value })}>
            {ctx.freightForwarders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
          </Select>
        </Field>
        <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>{VENDOR_ESTIMATE_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}</Select></Field>
        <Field label="Description"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Estimate Document (optional)"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
      </div>
    </Modal>
  );
}
function AddFreightEstimateRevisionModal({ open, fre, onClose, ctx, project }) {
  const [form, setForm] = useState({ amount: '', date: todayISO(), file: '', fileUrl: null, note: '' });
  useEffect(() => { if (open) setForm({ amount: '', date: todayISO(), file: '', fileUrl: null, note: '' }); }, [open]);
  if (!fre) return null;
  function submit() {
    if (!form.amount) return;
    ctx.addFreightEstimateRevision(project.id, fre.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Add Revision — ${fre.carrier}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Revision</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Quotation File"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Tasks -------------------------------------------------------------
function TasksTab({ ctx, project }) {
  const editable = ctx.canEdit('tasks');
  const [assignee, setAssignee] = useState(ctx.currentUserId);
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const filtered = project.tasks.filter(t => assignee === '__all__' || t.assigneeId === assignee);
  const buckets = { Overdue: [], Today: [], Upcoming: [], Completed: [] };
  filtered.forEach(t => buckets[taskBucket(t)].push(t));

  return (
    <div>
      <div className="flex items-center justify-between flex-wrap gap-2 mb-3">
        <Field label="Assigned To">
          <Select value={assignee} onChange={e => setAssignee(e.target.value)} className="!w-56">
            <option value={ctx.currentUserId}>Me ({ctx.currentUserName})</option>
            <option value="__all__">Everyone</option>
            {ctx.teamDirectory.filter(p => p.id !== ctx.currentUserId).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        {editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Task</Button>}
      </div>
      {['Overdue', 'Today', 'Upcoming', 'Completed'].map(bucket => (
        <Collapsible key={bucket} title={bucket} count={buckets[bucket].length}>
          {buckets[bucket].length === 0 ? <EmptyState text="Nothing here." /> : (
            <div className="space-y-1.5">
              {buckets[bucket].map(t => (
                <div key={t.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2">
                  <div className="flex items-center gap-2 min-w-0">
                    <input type="checkbox" checked={t.status === 'Completed'} onChange={e => ctx.setTaskStatus(project.id, t.id, e.target.checked ? 'Completed' : 'Open')} disabled={!editable} />
                    <div className="min-w-0 cursor-pointer" onClick={() => setDetailFor(t)}>
                      <p className="text-sm font-semibold truncate hover:underline">{t.title}</p>
                      <p className="text-xs text-[var(--leon-black)]/50">Due {fmtDate(t.dueDate)} · {personName(ctx.teamDirectory, t.assigneeId)}</p>
                    </div>
                  </div>
                  <Badge tone={t.priority === 'High' ? 'red' : t.priority === 'Medium' ? 'yellow' : 'neutral'}>{t.priority}</Badge>
                </div>
              ))}
            </div>
          )}
        </Collapsible>
      ))}
      <AddTaskModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <TaskDetailModal open={!!detailFor} task={detailFor} onClose={() => setDetailFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function TaskDetailModal({ open, task, onClose, ctx, project }) {
  if (!task) return null;
  const scope = task.scopeId ? project.scopes.find(s => s.id === task.scopeId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Task — ${task.title}`} printable
      fields={[
        { label: 'Status', value: task.status }, { label: 'Priority', value: task.priority },
        { label: 'Due Date', value: fmtDate(task.dueDate) }, { label: 'Assignee', value: personName(ctx.teamDirectory, task.assigneeId) },
        { label: 'Project', value: project.name }, { label: 'Scope', value: scope ? scope.name : '—' },
      ]}
      relatedRecords={scope ? [{ label: `Scope: ${scope.name}`, onClick: () => ctx.goProjectTab(project.id, 'scopes') }] : []}
    />
  );
}
function AddTaskModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ title: '', assigneeId: ctx.currentUserId, dueDate: todayISO(), priority: 'Medium', scopeId: '' });
  useEffect(() => { if (open) setForm({ title: '', assigneeId: ctx.currentUserId, dueDate: todayISO(), priority: 'Medium', scopeId: '' }); }, [open]);
  function submit() {
    if (!form.title.trim()) return;
    ctx.addTask(project.id, { ...form, scopeId: form.scopeId || null });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Add Task" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Task</Button></>}>
      <div className="space-y-3">
        <Field label="Title"><TextInput value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Assignee"><Select value={form.assigneeId} onChange={e => setForm({ ...form, assigneeId: e.target.value })}>{ctx.teamDirectory.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</Select></Field>
          <Field label="Due Date"><TextInput type="date" value={form.dueDate} onChange={e => setForm({ ...form, dueDate: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Priority"><Select value={form.priority} onChange={e => setForm({ ...form, priority: e.target.value })}>{['Low', 'Medium', 'High'].map(p => <option key={p}>{p}</option>)}</Select></Field>
          <Field label="Related Scope (optional)"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
      </div>
    </Modal>
  );
}

// ---- Issues -------------------------------------------------------------
function IssuesTab({ ctx, project }) {
  const editable = ctx.canEdit('issues');
  const [showAdd, setShowAdd] = useState(false);
  // Holds just the id, not the issue object itself — the object is looked up
  // fresh from `project.issues` on every render (below), so the inline
  // Reassign Scope/Reassign To selects in IssueDetailModal reflect an update
  // immediately instead of showing a stale snapshot from when it was opened.
  const [detailForId, setDetailForId] = useState(null);
  const [respondFor, setRespondFor] = useState(null);
  const detailFor = detailForId ? project.issues.find(x => x.id === detailForId) : null;
  const open = project.issues.filter(i => i.status === 'Open');
  const resolved = project.issues.filter(i => i.status === 'Resolved');
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Raise Issue</Button>}</div>
      <Collapsible title="Open" count={open.length}>
        {open.length === 0 ? <EmptyState text="No open issues." /> : open.map(i => <IssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailForId(i.id)} onRespond={() => setRespondFor(i)} />)}
      </Collapsible>
      <Collapsible title="Resolved" count={resolved.length}>
        {resolved.length === 0 ? <EmptyState text="None yet." /> : resolved.map(i => <IssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailForId(i.id)} onRespond={() => setRespondFor(i)} />)}
      </Collapsible>
      <AddIssueModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <IssueDetailModal open={!!detailFor} issue={detailFor} onClose={() => setDetailForId(null)} ctx={ctx} project={project} />
      <RespondIssueModal open={!!respondFor} issue={respondFor} onClose={() => setRespondFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function IssueRow({ i, ctx, project, editable, onOpen, onRespond }) {
  const scope = i.scopeId ? project.scopes.find(s => s.id === i.scopeId) : null;
  return (
    <div className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2 mb-1.5">
      <div className="min-w-0 cursor-pointer" onClick={onOpen}>
        <div className="flex items-center gap-2"><Badge tone={i.severity === 'High' ? 'red' : i.severity === 'Medium' ? 'yellow' : 'neutral'}>{i.severity}</Badge><p className="text-sm font-semibold truncate hover:underline">{i.title}</p></div>
        <p className="text-xs text-[var(--leon-black)]/50">{i.description}</p>
        <p className="text-[11px] text-[var(--leon-black)]/40">Raised {fmtDate(i.dateRaised)}{i.dateResolved ? ` · Resolved ${fmtDate(i.dateResolved)}` : ''}{scope ? ` · ${scope.name}` : ''}{i.assigneeId ? ` · Assigned: ${personName(ctx.teamDirectory, i.assigneeId)}` : ''}</p>
        {i.status === 'Resolved' && i.resolution && <p className="text-[11px] text-[var(--leon-brown)] mt-0.5">✓ {i.resolution}</p>}
        {i.status === 'Open' && i.followUpDueDate && (
          <p className="text-[11px] font-semibold text-[var(--leon-yellow)] mt-0.5">
            Follow-up due {fmtDate(i.followUpDueDate)}{i.followUpAssigneeId ? ` — ${personName(ctx.teamDirectory, i.followUpAssigneeId)}` : ''}
          </p>
        )}
        <IssueResponseThread issue={i} ctx={ctx} />
      </div>
      {editable && i.status === 'Open' && (
        <Button size="sm" variant="ghost" onClick={onRespond}>
          {(i.responses || []).length ? 'Add Response' : 'Respond'}
        </Button>
      )}
    </div>
  );
}
// Responding to an issue is rarely just "it's fixed" — most of the time it is
// a finding to record or a hand-off to someone who can act. The outcome picker
// makes that explicit, and attachments/follow-up hang off whichever is chosen.
function RespondIssueModal({ open, issue, onClose, ctx, project }) {
  const blank = { outcome: 'Update', text: '', followUpAssigneeId: '', followUpDueDate: '', attachments: [] };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, attachments: [] }); }, [open, issue && issue.id]);
  if (!issue) return null;

  const needsFollowUp = form.outcome === 'Needs Follow-Up';
  const canSubmit = form.text.trim() && (!needsFollowUp || form.followUpAssigneeId);

  function addFile(name, url) {
    if (!name || !url) return;
    setForm(f => ({ ...f, attachments: [...f.attachments, { name, url }] }));
  }
  function submit() {
    if (!canSubmit) return;
    ctx.respondToIssue(project.id, issue.id, {
      outcome: form.outcome,
      text: form.text,
      attachments: form.attachments,
      followUpAssigneeId: needsFollowUp ? form.followUpAssigneeId : null,
      followUpDueDate: needsFollowUp ? (form.followUpDueDate || null) : null,
    }, ctx.currentUserName);
    onClose();
  }

  const label = form.outcome === 'Resolved' ? 'Resolve Issue'
    : needsFollowUp ? 'Assign Follow-Up' : 'Post Update';

  return (
    <Modal open={open} onClose={onClose} wide title={`Respond — ${issue.title}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!canSubmit}>{label}</Button></>}>
      <div className="space-y-4">
        <div className="bg-[var(--leon-cream)] rounded-lg p-3">
          <div className="flex items-center gap-2 mb-1">
            <Badge tone={issue.severity === 'High' ? 'red' : issue.severity === 'Medium' ? 'yellow' : 'neutral'}>{issue.severity}</Badge>
            <span className="text-xs text-[var(--leon-black)]/50">Raised {fmtDate(issue.dateRaised)}</span>
          </div>
          <p className="text-sm">{issue.description}</p>
        </div>

        <div>
          <span className="block text-xs font-semibold text-[var(--leon-black)]/60 uppercase tracking-wide mb-1.5">What are you doing?</span>
          <div className="grid sm:grid-cols-3 gap-2">
            {ISSUE_RESPONSE_OUTCOMES.map(o => (
              <button key={o.key} type="button" onClick={() => setForm({ ...form, outcome: o.key })}
                className={`text-left p-2.5 rounded-lg border transition ${form.outcome === o.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown-light)]'}`}>
                <span className="block text-sm font-semibold">{o.label}</span>
                <span className="block text-[11px] text-[var(--leon-black)]/50 leading-snug mt-0.5">{o.hint}</span>
              </button>
            ))}
          </div>
        </div>

        <Field label={form.outcome === 'Resolved' ? 'How was this resolved?' : 'What happened / what is needed?'}>
          <TextArea rows={4} value={form.text} onChange={e => setForm({ ...form, text: e.target.value })} autoFocus />
        </Field>

        {needsFollowUp && (
          <div className="grid sm:grid-cols-2 gap-3 border-l-2 border-[var(--leon-brown)] pl-3">
            <Field label="Assign follow-up to" hint="They become the issue's assignee, so it reaches their To-Do.">
              <Select value={form.followUpAssigneeId} onChange={e => setForm({ ...form, followUpAssigneeId: e.target.value })}>
                <option value="">Select a person…</option>
                {ctx.teamDirectory.filter(p => p.active !== false).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              </Select>
            </Field>
            <Field label="Follow-up due">
              <TextInput type="date" value={form.followUpDueDate} onChange={e => setForm({ ...form, followUpDueDate: e.target.value })} />
            </Field>
          </div>
        )}

        <div>
          <span className="block text-xs font-semibold text-[var(--leon-black)]/60 uppercase tracking-wide mb-1.5">
            Attach photos or documents {form.attachments.length > 0 && <span className="text-[var(--leon-brown)]">({form.attachments.length})</span>}
          </span>
          {form.attachments.length > 0 && (
            <div className="flex flex-wrap gap-1.5 mb-2">
              {form.attachments.map((a, idx) => (
                <span key={idx} className="inline-flex items-center gap-1 text-xs bg-[var(--leon-cream)] border border-[var(--leon-line)] rounded px-2 py-1">
                  {/^data:image|\.(png|jpe?g|gif|webp)$/i.test(a.url) && <Photo src={a.url} alt={a.name || ''} title={a.name || 'Attachment'} className="w-6 h-6 object-cover rounded" />}
                  <span className="max-w-[160px] truncate">{a.name}</span>
                  <IconBtn title="Remove" onClick={() => setForm(f => ({ ...f, attachments: f.attachments.filter((_, j) => j !== idx) }))}>✕</IconBtn>
                </span>
              ))}
            </div>
          )}
          <FileField name="" url={null} editable onChange={(name, url) => addFile(name, url)} />
          <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">Site photos, a marked-up drawing, a supplier email &mdash; anything that evidences the response.</p>
        </div>
      </div>
    </Modal>
  );
}

// The running record of what has been said and done on an issue.
function IssueResponseThread({ issue, ctx }) {
  const responses = issue.responses || [];
  if (!responses.length) return null;
  const tone = o => o === 'Resolved' ? 'green' : o === 'Needs Follow-Up' ? 'yellow' : 'neutral';
  return (
    <div className="mt-2 space-y-1.5">
      {responses.map(r => (
        <div key={r.id} className="border-l-2 border-[var(--leon-line)] pl-2.5 py-0.5">
          <div className="flex items-center gap-2 flex-wrap">
            <Badge tone={tone(r.outcome)}>{r.outcome}</Badge>
            <span className="text-[11px] text-[var(--leon-black)]/45">{r.by} &middot; {fmtDate(r.date)}</span>
            {r.followUpAssigneeId && (
              <span className="text-[11px] text-[var(--leon-brown)] font-semibold">
                &rarr; {personName(ctx.teamDirectory, r.followUpAssigneeId)}{r.followUpDueDate ? ` by ${fmtDate(r.followUpDueDate)}` : ''}
              </span>
            )}
          </div>
          <p className="text-xs text-[var(--leon-black)]/70 mt-0.5">{r.text}</p>
          {(r.attachments || []).length > 0 && (
            <div className="flex flex-wrap gap-1 mt-1">
              {r.attachments.map(a => (
                <a key={a.id} href={a.url} target="_blank" rel="noopener noreferrer"
                   className="text-[11px] text-[var(--leon-brown)] hover:underline inline-flex items-center gap-1">
                  📎 {a.name}
                </a>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}
function IssueDetailModal({ open, issue, onClose, ctx, project }) {
  if (!issue) return null;
  const editable = ctx.canEdit('issues');
  const scope = issue.scopeId ? project.scopes.find(s => s.id === issue.scopeId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Issue — ${issue.title}`} printable
      fields={[
        { label: 'Severity', value: issue.severity }, { label: 'Status', value: issue.status },
        { label: 'Date Raised', value: fmtDate(issue.dateRaised) }, { label: 'Date Resolved', value: issue.dateResolved ? fmtDate(issue.dateResolved) : '—' },
        { label: 'Project', value: project.name }, { label: 'Description', value: issue.description },
        { label: 'Scope', value: scope ? scope.name : '—' }, { label: 'Assigned To', value: issue.assigneeId ? personName(ctx.teamDirectory, issue.assigneeId) : '—' },
        ...(issue.status === 'Resolved' ? [{ label: 'How It Was Resolved', value: issue.resolution || '—' }, { label: 'Resolved By', value: issue.resolvedBy || '—' }] : []),
      ]}
      attachments={issue.attachments}
    >
      {editable && (
        <div className="no-print grid sm:grid-cols-2 gap-3">
          <Field label="Reassign Scope">
            <Select value={issue.scopeId || ''} onChange={e => ctx.updateIssue(project.id, issue.id, { scopeId: e.target.value || null })}>
              <option value="">— none —</option>
              {project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
          <Field label="Reassign To">
            <Select value={issue.assigneeId || ''} onChange={e => ctx.updateIssue(project.id, issue.id, { assigneeId: e.target.value || null })}>
              <option value="">— unassigned —</option>
              {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <div className="sm:col-span-2">
            <p className="text-xs font-semibold mb-1">Add Attachment</p>
            <div className="space-y-1.5">
              {issue.attachments.map(a => (
                <div key={a.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded-md px-2 py-1">
                  <span>{a.name}</span>
                  <IconBtn title="Remove" onClick={() => ctx.removeIssueAttachment(project.id, issue.id, a.id)}>✕</IconBtn>
                </div>
              ))}
              <FileField name="" url={null} onChange={(fname, url) => ctx.addIssueAttachment(project.id, issue.id, fname, url)} editable placeholder="+ Add attachment" />
            </div>
          </div>
          {issue.status === 'Resolved' && (
            <div className="sm:col-span-2">
              <Button size="sm" variant="ghost" onClick={() => ctx.setIssueStatus(project.id, issue.id, 'Open')}>Reopen Issue</Button>
            </div>
          )}
        </div>
      )}
    </RecordDetailModal>
  );
}
function AddIssueModal({ open, onClose, ctx, project }) {
  const blank = { title: '', description: '', severity: 'Medium', scopeId: '', assigneeId: '', attachments: [] };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function submit() {
    if (!form.title.trim()) return;
    ctx.addIssue(project.id, { ...form, scopeId: form.scopeId || null, assigneeId: form.assigneeId || null });
    onClose();
  }
  function addAttachment(fname, url) { setForm(f => ({ ...f, attachments: [...f.attachments, { id: uid('att'), name: fname, url, uploadedBy: ctx.currentUserName, uploadedDate: todayISO() }] })); }
  function removeAttachment(id) { setForm(f => ({ ...f, attachments: f.attachments.filter(a => a.id !== id) })); }
  return (
    <Modal open={open} onClose={onClose} title="Raise Issue" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Raise Issue</Button></>}>
      <div className="space-y-3">
        <Field label="Title"><TextInput value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} /></Field>
        <Field label="Description"><TextArea rows={3} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Severity"><Select value={form.severity} onChange={e => setForm({ ...form, severity: e.target.value })}>{['Low', 'Medium', 'High'].map(s => <option key={s}>{s}</option>)}</Select></Field>
          <Field label="Related Scope (optional)"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}><option value="">—</option>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
          <Field label="Assigned To (optional)">
            <Select value={form.assigneeId} onChange={e => setForm({ ...form, assigneeId: e.target.value })}>
              <option value="">—</option>
              {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Attachments">
          <div className="space-y-1.5">
            {form.attachments.map(a => (
              <div key={a.id} className="flex items-center justify-between text-xs border border-[var(--leon-line)] rounded-md px-2 py-1">
                <span>{a.name}</span>
                <IconBtn title="Remove" onClick={() => removeAttachment(a.id)}>✕</IconBtn>
              </div>
            ))}
            <FileField name="" url={null} onChange={addAttachment} editable placeholder="+ Add attachment" />
          </div>
        </Field>
      </div>
    </Modal>
  );
}

// ---- Installation / Field Ops ----------------------------------------------
// Ordered to follow the actual sequence of work on site: you measure, you
// install, you punch out, and the daily report / field issues are the running
// record alongside it.
const INSTALLATION_SUBTABS = [
  { key: 'fieldMeasurements', label: 'Measurements', icon: '📐' },
  { key: 'records', label: 'Installation', icon: '🔧' },
  { key: 'punch', label: 'Punch List', icon: '✔️' },
  { key: 'daily', label: 'Daily Report', icon: '📝' },
  { key: 'fieldIssues', label: 'Field Issues', icon: '⚠️' },
];
function InstallationTab({ ctx, project, pendingNav }) {
  return (
    <ScopeHubTabs
      ctx={ctx}
      project={project}
      overview={<InstallationHubOverview ctx={ctx} project={project} pendingNav={pendingNav} />}
      renderScope={scope => <InstallationScopePanel ctx={ctx} project={project} scope={scope} />}
    />
  );
}
function InstallationHubOverview({ ctx, project, pendingNav }) {
  const [sub, setSub] = useState((pendingNav && pendingNav.subtab) || 'records');
  useEffect(() => { if (pendingNav && pendingNav.subtab) setSub(pendingNav.subtab); }, [pendingNav]);
  return (
    <div>
      <Tabs tabs={INSTALLATION_SUBTABS} active={sub} onChange={setSub} />
      <div className="pt-4">
        {sub === 'records' && <InstallationRecordsPanel ctx={ctx} project={project} />}
        {sub === 'daily' && <DailyFieldReportsPanel ctx={ctx} project={project} />}
        {sub === 'fieldIssues' && <FieldIssuesPanel ctx={ctx} project={project} />}
        {sub === 'punch' && <PunchListPanel ctx={ctx} project={project} />}
        {sub === 'fieldMeasurements' && <FieldMeasurementPanel ctx={ctx} project={project} />}
      </div>
    </div>
  );
}

// ---- Per-scope Installation Hub panel (Phase 6) — Overview / Field
// Measurement / Installation Scheduled / Report Issues / Punch Lists ----
const INSTALLATION_SCOPE_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  { key: 'fieldMeasurement', label: 'Field Measurement', icon: '📐' },
  { key: 'scheduled', label: 'Installation Scheduled', icon: '📅' },
  { key: 'issues', label: 'Report Issues', icon: '⚠️' },
  { key: 'punch', label: 'Punch Lists', icon: '✔️' },
];
function InstallationScopePanel({ ctx, project, scope }) {
  const [sub, setSub] = useState('overview');
  const records = project.installationRecords.filter(r => r.scopeId === scope.id);
  const items = project.punchItems.filter(p => p.scopeId === scope.id);
  const issues = project.fieldIssues.filter(i => i.scopeId === scope.id);
  const threads = project.fieldMeasurements.filter(t => t.scopeId === scope.id);
  return (
    <div>
      <Tabs tabs={INSTALLATION_SCOPE_SUBTABS} active={sub} onChange={setSub} />
      <div className="pt-4">
        {sub === 'overview' && <InstallationScopeOverview project={project} scope={scope} records={records} items={items} threads={threads} />}
        {sub === 'fieldMeasurement' && <ScopeFieldMeasurementSubTab ctx={ctx} project={project} scope={scope} threads={threads} />}
        {sub === 'scheduled' && <InstallationScheduledSubTab ctx={ctx} project={project} scope={scope} records={records} />}
        {sub === 'issues' && <ScopeReportIssuesSubTab ctx={ctx} project={project} scope={scope} issues={issues} />}
        {sub === 'punch' && <ScopePunchListSubTab ctx={ctx} project={project} scope={scope} items={items} />}
      </div>
    </div>
  );
}
function InstallationScopeOverview({ project, scope, records, items, threads }) {
  const complete = records.filter(r => r.status === 'Installation Complete' || r.status === 'Approved/Closed').length;
  const openPunch = items.filter(p => p.status !== 'Closed').length;
  const na = !!project.fieldMeasurementNA[scope.id];
  const fmValue = na ? 'N/A' : threads.length === 0 ? 'Not Started' : `${threads.length} report(s)`;
  return (
    <div className="grid sm:grid-cols-4 gap-3">
      <StatBox label="Installation Records" value={String(records.length)} />
      <StatBox label="Completed" value={`${complete}/${records.length}`} tone={records.length > 0 && complete === records.length ? 'green' : undefined} />
      <StatBox label="Open Punch Items" value={String(openPunch)} tone={openPunch ? 'yellow' : undefined} />
      <StatBox label="Field Measurement" value={fmValue} />
    </div>
  );
}
function ScopeFieldMeasurementSubTab({ ctx, project, scope, threads }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const na = !!project.fieldMeasurementNA[scope.id];
  return (
    <div>
      <label className="flex items-center gap-2 text-sm mb-3">
        <input type="checkbox" checked={na} disabled={!editable} onChange={e => ctx.setFieldMeasurementNA(project.id, scope.id, e.target.checked)} />
        Not Applicable for this scope
      </label>
      {na ? (
        <Badge tone="neutral">Field Measurement — Not Applicable for this scope</Badge>
      ) : (
        <>
          <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ New Field Measurement Report</Button>}</div>
          <Collapsible title="Field Measurement Report" count={threads.length}>
            {threads.length === 0 ? <EmptyState text="No field measurement reports yet for this scope." /> : (
              <div className="space-y-2">{threads.map(t => <FieldMeasurementThreadCard key={t.id} ctx={ctx} project={project} thread={t} editable={editable} />)}</div>
            )}
          </Collapsible>
          <AddFieldMeasurementThreadModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} defaultScopeId={scope.id} />
        </>
      )}
    </div>
  );
}
function installationApprovalTone(status) {
  if (status === 'Approved') return 'green';
  if (status === 'Reschedule Requested') return 'yellow';
  return 'neutral';
}
function InstallationScheduledSubTab({ ctx, project, scope, records }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [rescheduleFor, setRescheduleFor] = useState(null);
  const [requestRescheduleFor, setRequestRescheduleFor] = useState(null);
  const [completeFor, setCompleteFor] = useState(null);
  const paymentHold = projectPaymentHold(project, 'Installations');
  return (
    <div>
      {paymentHold && (
        <div className="mb-4 border border-[var(--leon-red)] bg-[var(--leon-red)]/5 rounded-lg p-3">
          <p className="text-sm font-bold text-[var(--leon-red)]">⚠ Hold — Payment Not Received</p>
          <p className="text-xs text-[var(--leon-black)]/60 mt-0.5">{paymentHold.description}</p>
        </div>
      )}
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Schedule Installation</Button>}</div>
      <Collapsible title="Installation Scheduled" count={records.length}>
        {records.length === 0 ? <EmptyState text="No installations scheduled for this scope yet." /> : (
          <div className="space-y-2">
            {records.map(rec => {
              const sub = ctx.subcontractors.find(s => s.id === rec.assignedSubcontractorId);
              return (
                <div key={rec.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-start justify-between gap-2 flex-wrap mb-1">
                    <div>
                      <p className="text-sm font-bold">{[rec.building, rec.floor && `Fl. ${rec.floor}`, rec.unit, rec.room].filter(Boolean).join(' · ')}{rec.item ? ` — ${rec.item}` : ''}</p>
                      <p className="text-xs text-[var(--leon-black)]/50">Assigned: {sub ? sub.companyName : (rec.assignedCrew || 'Unassigned')} · Scheduled {fmtDate(rec.scheduledStart)}{rec.scheduledTime ? ` ${rec.scheduledTime}` : ''}</p>
                    </div>
                    <div className="flex items-center gap-1.5">
                      <StatusBadge status={rec.status} />
                      <Badge tone={installationApprovalTone(rec.approvalStatus)}>{rec.approvalStatus}</Badge>
                    </div>
                  </div>
                  {rec.rescheduleRequest && rec.approvalStatus === 'Reschedule Requested' && (
                    <p className="text-xs text-[var(--leon-red)] mb-1.5">Installer proposed {fmtDate(rec.rescheduleRequest.proposedDate)}{rec.rescheduleRequest.proposedTime ? ` ${rec.rescheduleRequest.proposedTime}` : ''}{rec.rescheduleRequest.reason ? ` — ${rec.rescheduleRequest.reason}` : ''}</p>
                  )}
                  {rec.outcome && (
                    <p className="text-xs text-[var(--leon-black)]/50 mb-1.5">Last logged outcome: <strong>{rec.outcome}</strong> ({rec.pctComplete || 0}%){rec.completionNotes ? ` — ${rec.completionNotes}` : ''}</p>
                  )}
                  {rec.returnVisit && rec.returnVisit.date && (
                    <p className="text-xs text-[var(--leon-brown)] font-semibold mb-1.5">Return visit scheduled: {fmtDate(rec.returnVisit.date)}{rec.returnVisit.time ? ` ${rec.returnVisit.time}` : ''}{rec.returnVisit.durationMinutes ? ` · ~${rec.returnVisit.durationMinutes} min` : ''}</p>
                  )}
                  {editable && (
                    <div className="flex items-center gap-2 flex-wrap mt-1.5">
                      {rec.approvalStatus === 'Pending Installer Approval' && (
                        <>
                          <Button size="sm" onClick={() => ctx.approveInstallationSchedule(project.id, rec.id)}>Approve Schedule</Button>
                          <Button size="sm" variant="ghost" onClick={() => setRequestRescheduleFor(rec)}>Request Reschedule</Button>
                        </>
                      )}
                      {rec.approvalStatus === 'Reschedule Requested' && <Button size="sm" onClick={() => setRescheduleFor(rec)}>Resolve Reschedule</Button>}
                      {rec.approvalStatus === 'Approved' && <Button size="sm" variant="ghost" onClick={() => setRescheduleFor(rec)}>Reschedule</Button>}
                      {rec.approvalStatus === 'Approved' && rec.status !== 'Approved/Closed' && <Button size="sm" variant="outline" onClick={() => setCompleteFor(rec)}>Log Completion</Button>}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddInstallationRecordModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} defaultScopeId={scope.id} />
      <RescheduleInstallationModal open={!!rescheduleFor} rec={rescheduleFor} onClose={() => setRescheduleFor(null)} ctx={ctx} project={project} />
      <RequestInstallationRescheduleModal open={!!requestRescheduleFor} rec={requestRescheduleFor} onClose={() => setRequestRescheduleFor(null)} ctx={ctx} project={project} />
      <LogInstallationCompletionModal open={!!completeFor} rec={completeFor} onClose={() => setCompleteFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function ScopeReportIssuesSubTab({ ctx, project, scope, issues }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const open = issues.filter(i => i.status === 'Open');
  const resolved = issues.filter(i => i.status !== 'Open');
  return (
    <div>
      {editable && (
        <div className="flex justify-center mb-4">
          <Button size="lg" variant="danger" onClick={() => setShowAdd(true)}>🚨 Report an Issue</Button>
        </div>
      )}
      <Collapsible title="Open" count={open.length}>
        {open.length === 0 ? <EmptyState text="No open field issues for this scope." /> : open.map(i => <FieldIssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailFor(i)} />)}
      </Collapsible>
      <Collapsible title="Resolved" count={resolved.length}>
        {resolved.length === 0 ? <EmptyState text="None yet." /> : resolved.map(i => <FieldIssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailFor(i)} />)}
      </Collapsible>
      <AddFieldIssueModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} defaultScopeId={scope.id} />
      <FieldIssueDetailModal open={!!detailFor} issue={detailFor} onClose={() => setDetailFor(null)} project={project} />
    </div>
  );
}
function ScopePunchListSubTab({ ctx, project, scope, items }) {
  const editable = ctx.canEdit('installation');
  const canClose = ['Admin', 'Accounting', 'General Manager', 'Project Coordinator'].includes(ctx.currentRole);
  const [showAdd, setShowAdd] = useState(false);
  const [respondFor, setRespondFor] = useState(null);
  const [detailFor, setDetailFor] = useState(null);
  const [scheduleFor, setScheduleFor] = useState(null);
  const [rejectFor, setRejectFor] = useState(null);
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Punch Item</Button>}</div>
      <Collapsible title="Punch List" count={items.length}>
        {items.length === 0 ? <EmptyState text="No punch items yet for this scope." /> : (
          <div className="space-y-2">
            {items.map(p => {
              const sub = ctx.subcontractors.find(s => s.id === p.assignedSubcontractorId);
              return (
                <div key={p.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                  <div className="flex items-center justify-between gap-2 flex-wrap cursor-pointer" onClick={() => setDetailFor(p)}>
                    <p className="text-sm font-semibold hover:underline">{[p.building, p.floor, p.unit, p.room, p.item].filter(Boolean).join(' · ')}</p>
                    <div className="flex items-center gap-2">
                      <Badge tone={p.priority === 'High' ? 'red' : p.priority === 'Medium' ? 'yellow' : 'neutral'}>{p.priority}</Badge>
                      <StatusBadge status={p.status} />
                    </div>
                  </div>
                  <p className="text-xs text-[var(--leon-black)]/50">{p.problem} · Assigned: {sub ? sub.companyName : (p.responsibleParty || '—')} · Target {fmtDate(p.targetDate)}</p>
                  {p.scheduledReturnDate && (
                    <p className="text-xs text-[var(--leon-brown)] font-semibold mt-0.5">Return scheduled: {fmtDate(p.scheduledReturnDate)}{p.scheduledReturnTime ? ` ${p.scheduledReturnTime}` : ''}{p.scheduledReturnDuration ? ` · ~${p.scheduledReturnDuration} min` : ''}</p>
                  )}
                  {p.lastRejectionNote && <p className="text-xs text-[var(--leon-red)] mt-0.5">Rejected — {p.lastRejectionNote || 'work did not meet approval, please reschedule a return.'}</p>}
                  <div className="flex items-center gap-2 mt-1">
                    {p.photoBefore && <ClickableImage src={p.photoBefore} name="Punch Item — Before" className="w-14 h-14 object-cover rounded-md" />}
                    {p.photoAfter && <ClickableImage src={p.photoAfter} name="Punch Item — After" className="w-14 h-14 object-cover rounded-md" />}
                  </div>

                  {p.responseStatus && (
                    <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
                      <div className="flex items-center gap-1.5 flex-wrap">
                        <Badge tone={punchResponseTone(p.responseStatus)}>{p.responseStatus}</Badge>
                        <span className="text-[11px] text-[var(--leon-black)]/40">by {p.respondedBy}, {fmtDate(p.respondedDate)}{p.repairCompletedDate ? ` · repaired ${fmtDate(p.repairCompletedDate)}` : ''}</span>
                      </div>
                      {p.responseNotes && <p className="text-xs text-[var(--leon-black)]/60 mt-1">{p.responseNotes}</p>}
                    </div>
                  )}

                  {editable && p.status !== 'Closed' && (
                    <div className="flex gap-2 mt-2 items-center flex-wrap">
                      {!p.scheduledReturnDate && <Button size="sm" variant="outline" onClick={() => setScheduleFor(p)}>Schedule Return</Button>}
                      {p.scheduledReturnDate && <Button size="sm" onClick={() => setRespondFor(p)}>Respond</Button>}
                      {p.status === 'Completed – Awaiting Verification' && canClose && (
                        <>
                          <Button size="sm" variant="black" onClick={() => ctx.decidePunchResponse(project.id, p.id, 'Approved')}>Work Approved</Button>
                          <Button size="sm" variant="danger" onClick={() => setRejectFor(p)}>Work Rejected</Button>
                        </>
                      )}
                      {p.status === 'Completed – Awaiting Verification' && !canClose && <span className="text-[11px] text-[var(--leon-black)]/40 italic">Awaiting PC approval.</span>}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddPunchItemModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} defaultScopeId={scope.id} />
      <RespondPunchItemModal open={!!respondFor} item={respondFor} onClose={() => setRespondFor(null)} ctx={ctx} project={project} />
      <PunchItemDetailModal open={!!detailFor} item={detailFor} onClose={() => setDetailFor(null)} />
      <SchedulePunchReturnModal open={!!scheduleFor} item={scheduleFor} onClose={() => setScheduleFor(null)} ctx={ctx} project={project} />
      <RejectPunchResponseModal open={!!rejectFor} item={rejectFor} onClose={() => setRejectFor(null)} ctx={ctx} project={project} />
    </div>
  );
}
function SchedulePunchReturnModal({ open, item, onClose, ctx, project }) {
  const blank = { date: todayISO(), time: '', durationMinutes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open, item]);
  if (!item) return null;
  function submit() { ctx.schedulePunchReturn(project.id, item.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Schedule Return — ${item.item || 'Punch Item'}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="grid grid-cols-3 gap-3">
        <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        <Field label="Time"><TextInput type="time" value={form.time} onChange={e => setForm({ ...form, time: e.target.value })} /></Field>
        <Field label="Duration (min)"><TextInput type="number" value={form.durationMinutes} onChange={e => setForm({ ...form, durationMinutes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
function RejectPunchResponseModal({ open, item, onClose, ctx, project }) {
  const [note, setNote] = useState('');
  useEffect(() => { if (open) setNote(''); }, [open, item]);
  if (!item) return null;
  function submit() { ctx.decidePunchResponse(project.id, item.id, 'Rejected', note); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Reject Work — ${item.item || 'Punch Item'}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit}>Reject &amp; Reopen</Button></>}>
      <Field label="Reason" hint="Sent back to Open — the subcontractor will need to schedule another return."><TextArea rows={3} value={note} onChange={e => setNote(e.target.value)} /></Field>
    </Modal>
  );
}

function InstallationRecordsPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const installers = ctx.teamDirectory.filter(p => p.securityRole === 'Installation Team & Field Foreman');
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Installation Record</Button>}</div>
      <Collapsible id={`${project.id}-installation-records`} title="Installation Records" count={project.installationRecords.length}>
      {project.installationRecords.length === 0 ? <EmptyState text="No installation records yet." /> : (
        <div className="space-y-2">
          {project.installationRecords.map(rec => {
            const scope = project.scopes.find(s => s.id === rec.scopeId);
            return (
              <div key={rec.id} className="border border-[var(--leon-line)] rounded-lg p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap mb-1.5">
                  <div className="cursor-pointer" onClick={() => setDetailFor(rec)}>
                    <p className="text-sm font-bold hover:underline">{[rec.building, rec.floor && `Fl. ${rec.floor}`, rec.unit, rec.room].filter(Boolean).join(' · ')}</p>
                    <p className="text-xs text-[var(--leon-black)]/50">{scope ? scope.name : ''}{rec.item ? ` — ${rec.item}` : ''} · Crew: {rec.assignedCrew || 'Unassigned'}</p>
                  </div>
                  <StatusBadge status={rec.status} />
                </div>
                <div className="grid sm:grid-cols-4 gap-2 text-xs text-[var(--leon-black)]/50 mb-1.5">
                  <span>Sched. Start: <strong>{fmtDate(rec.scheduledStart)}</strong></span>
                  <span>Actual Start: <strong>{fmtDate(rec.actualStart)}</strong></span>
                  <span>Sched. Complete: <strong>{fmtDate(rec.scheduledCompletion)}</strong></span>
                  <span>Actual Complete: <strong>{fmtDate(rec.actualCompletion)}</strong></span>
                </div>
                <div className="flex items-center gap-1.5 mb-1.5">
                  <span className="text-xs text-[var(--leon-black)]/50">Assigned Subcontractor:</span>
                  {editable ? (
                    <Select value={rec.assignedSubcontractorId || ''} onChange={e => ctx.updateInstallationRecord(project.id, rec.id, { assignedSubcontractorId: e.target.value || null })} className="!w-56 !py-1 !text-xs">
                      <option value="">— none —</option>
                      {ctx.subcontractors.filter(s => s.status === 'Active').map(s => <option key={s.id} value={s.id}>{s.companyName}</option>)}
                    </Select>
                  ) : (
                    <span className="text-xs font-semibold">{(ctx.subcontractors.find(s => s.id === rec.assignedSubcontractorId) || {}).companyName || 'Unassigned'}</span>
                  )}
                </div>
                {editable && (
                  <div className="flex items-center gap-2 flex-wrap">
                    <Select value={rec.status} onChange={e => ctx.setInstallationStatus(project.id, rec.id, e.target.value)} className="!w-56 !py-1 !text-xs">
                      {INSTALLATION_STATUS_FLOW.map(s => <option key={s}>{s}</option>)}
                    </Select>
                    <Field label=""><TextInput type="number" min="0" max="100" value={rec.pctComplete} onChange={e => ctx.updateInstallationRecord(project.id, rec.id, { pctComplete: Number(e.target.value) })} className="!w-20 !py-1 !text-xs" /></Field>
                    <span className="text-xs text-[var(--leon-black)]/40">% complete</span>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
      </Collapsible>
      <AddInstallationRecordModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <InstallationRecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} rec={detailFor} project={project} ctx={ctx} />
    </div>
  );
}
function InstallationRecordDetailModal({ open, onClose, rec, project, ctx }) {
  if (!rec) return null;
  const scope = project.scopes.find(s => s.id === rec.scopeId);
  const sub = ctx.subcontractors.find(s => s.id === rec.assignedSubcontractorId);
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Installation — ${[rec.building, rec.floor && `Fl. ${rec.floor}`, rec.unit, rec.room].filter(Boolean).join(' · ')}`} printable
      fields={[
        { label: 'Project', value: project.name }, { label: 'Scope', value: scope ? scope.name : '—' },
        { label: 'Item / Type', value: rec.item }, { label: 'Status', value: rec.status }, { label: '% Complete', value: `${rec.pctComplete || 0}%` },
        { label: 'Assigned Crew', value: rec.assignedCrew || 'Unassigned' }, { label: 'Assigned Subcontractor', value: sub ? sub.companyName : 'Unassigned' },
        { label: 'Scheduled Start', value: fmtDate(rec.scheduledStart) }, { label: 'Actual Start', value: fmtDate(rec.actualStart) },
        { label: 'Scheduled Completion', value: fmtDate(rec.scheduledCompletion) }, { label: 'Actual Completion', value: fmtDate(rec.actualCompletion) },
      ]}
      relatedRecords={sub ? [{ label: `Subcontractor: ${sub.companyName}`, onClick: () => ctx.goSubcontractorDetail(sub.id) }] : []}
    />
  );
}
function AddInstallationRecordModal({ open, onClose, ctx, project, defaultScopeId }) {
  const installers = ctx.teamDirectory.filter(p => p.securityRole === 'Installation Team & Field Foreman');
  const blank = { scopeId: '', building: '', floor: '', unit: '', room: '', item: '', assignedCrew: '', assignedSubcontractorId: '', scheduledStart: todayISO(), scheduledTime: '', durationMinutes: '', scheduledCompletion: todayISO() };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, scopeId: defaultScopeId || project.scopes[0]?.id || '', assignedCrew: installers[0]?.name || '' }); }, [open, defaultScopeId]);
  function submit() { ctx.addInstallationRecord(project.id, { ...form, assignedSubcontractorId: form.assignedSubcontractorId || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Schedule Installation" wide footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Schedule &amp; Assign</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">The assigned installer will need to approve this schedule (or request a reschedule) before it's confirmed.</p>
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Building"><TextInput value={form.building} onChange={e => setForm({ ...form, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Unit/Area"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
          <Field label="Room"><TextInput value={form.room} onChange={e => setForm({ ...form, room: e.target.value })} /></Field>
        </div>
        <Field label="Item/Type"><TextInput value={form.item} onChange={e => setForm({ ...form, item: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Assigned Crew (internal)">
            <Select value={form.assignedCrew} onChange={e => setForm({ ...form, assignedCrew: e.target.value })}>
              <option value="">— none —</option>
              {installers.map(p => <option key={p.id} value={p.name}>{p.name}</option>)}
            </Select>
          </Field>
          <Field label="Assigned Subcontractor">
            <Select value={form.assignedSubcontractorId} onChange={e => setForm({ ...form, assignedSubcontractorId: e.target.value })}>
              <option value="">— none —</option>
              {ctx.subcontractors.filter(s => s.status === 'Active').map(s => <option key={s.id} value={s.id}>{s.companyName}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Scheduled Start"><TextInput type="date" value={form.scheduledStart} onChange={e => setForm({ ...form, scheduledStart: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.scheduledTime} onChange={e => setForm({ ...form, scheduledTime: e.target.value })} /></Field>
          <Field label="Duration (min)"><TextInput type="number" value={form.durationMinutes} onChange={e => setForm({ ...form, durationMinutes: e.target.value })} /></Field>
          <Field label="Scheduled Completion"><TextInput type="date" value={form.scheduledCompletion} onChange={e => setForm({ ...form, scheduledCompletion: e.target.value })} /></Field>
        </div>
      </div>
    </Modal>
  );
}
// Shared by the PC's initial schedule (via AddInstallationRecordModal above)
// and this later reschedule — same fields, reused for both per the Delivery
// Approve/Reschedule pattern, and both re-arm the installer's approval gate.
function RescheduleInstallationModal({ open, rec, onClose, ctx, project }) {
  const blank = { scheduledStart: '', scheduledTime: '', durationMinutes: '', scheduledCompletion: '', assignedSubcontractorId: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (open && rec) {
      setForm({
        scheduledStart: (rec.rescheduleRequest && rec.rescheduleRequest.proposedDate) || rec.scheduledStart || todayISO(),
        scheduledTime: (rec.rescheduleRequest && rec.rescheduleRequest.proposedTime) || rec.scheduledTime || '',
        durationMinutes: rec.durationMinutes || '', scheduledCompletion: rec.scheduledCompletion || todayISO(),
        assignedSubcontractorId: rec.assignedSubcontractorId || '',
      });
    }
  }, [open, rec]);
  if (!rec) return null;
  function submit() { ctx.rescheduleInstallation(project.id, rec.id, { ...form, assignedSubcontractorId: form.assignedSubcontractorId || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Reschedule Installation" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Schedule</Button></>}>
      <div className="space-y-3">
        {rec.rescheduleRequest && rec.rescheduleRequest.reason && <p className="text-xs text-[var(--leon-black)]/50">Installer's reason: {rec.rescheduleRequest.reason}</p>}
        <div className="grid grid-cols-3 gap-3">
          <Field label="Scheduled Start"><TextInput type="date" value={form.scheduledStart} onChange={e => setForm({ ...form, scheduledStart: e.target.value })} /></Field>
          <Field label="Time"><TextInput type="time" value={form.scheduledTime} onChange={e => setForm({ ...form, scheduledTime: e.target.value })} /></Field>
          <Field label="Duration (min)"><TextInput type="number" value={form.durationMinutes} onChange={e => setForm({ ...form, durationMinutes: e.target.value })} /></Field>
        </div>
        <Field label="Assigned Subcontractor">
          <Select value={form.assignedSubcontractorId} onChange={e => setForm({ ...form, assignedSubcontractorId: e.target.value })}>
            <option value="">— none —</option>
            {ctx.subcontractors.filter(s => s.status === 'Active').map(s => <option key={s.id} value={s.id}>{s.companyName}</option>)}
          </Select>
        </Field>
      </div>
    </Modal>
  );
}
// The installer's side of the approval gate — proposes a different date
// instead of accepting the PC's schedule outright.
function RequestInstallationRescheduleModal({ open, rec, onClose, ctx, project }) {
  const blank = { proposedDate: '', proposedTime: '', reason: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open && rec) setForm({ proposedDate: rec.scheduledStart || todayISO(), proposedTime: rec.scheduledTime || '', reason: '' }); }, [open, rec]);
  if (!rec) return null;
  function submit() { ctx.requestInstallationReschedule(project.id, rec.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Request Reschedule" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Send Request</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Proposed Date"><TextInput type="date" value={form.proposedDate} onChange={e => setForm({ ...form, proposedDate: e.target.value })} /></Field>
          <Field label="Proposed Time"><TextInput type="time" value={form.proposedTime} onChange={e => setForm({ ...form, proposedTime: e.target.value })} /></Field>
        </div>
        <Field label="Reason"><TextArea rows={2} value={form.reason} onChange={e => setForm({ ...form, reason: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
// The installer's on-site outcome — done/partial with notes/photos, and if
// partial, their own return visit (date/time/duration), matching the
// Delivery outcome pattern.
function LogInstallationCompletionModal({ open, rec, onClose, ctx, project }) {
  const blank = { outcome: 'Complete', pctComplete: 100, completionNotes: '', photos: [], returnDate: todayISO(), returnTime: '', returnDuration: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open && rec) setForm({ ...blank, pctComplete: rec.pctComplete || 100 }); }, [open, rec]);
  if (!rec) return null;
  function addPhoto(url) { setForm(f => ({ ...f, photos: [...f.photos, url] })); }
  function removePhoto(i) { setForm(f => ({ ...f, photos: f.photos.filter((_, idx) => idx !== i) })); }
  function submit() {
    ctx.logInstallationCompletion(project.id, rec.id, {
      outcome: form.outcome, completionNotes: form.completionNotes, pctComplete: form.pctComplete, photos: form.photos,
      returnVisit: form.outcome === 'Partial' ? { date: form.returnDate, time: form.returnTime, durationMinutes: form.returnDuration } : null,
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Log Installation Completion" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <Field label="Outcome">
          <Select value={form.outcome} onChange={e => setForm({ ...form, outcome: e.target.value })}>
            <option value="Complete">Complete</option>
            <option value="Partial">Partial</option>
          </Select>
        </Field>
        <Field label="% Complete"><TextInput type="number" min="0" max="100" value={form.pctComplete} onChange={e => setForm({ ...form, pctComplete: Number(e.target.value) })} /></Field>
        <Field label="Notes"><TextArea rows={2} value={form.completionNotes} onChange={e => setForm({ ...form, completionNotes: e.target.value })} /></Field>
        <div>
          <p className="text-xs font-semibold mb-1">Photos</p>
          <div className="flex items-center gap-2 flex-wrap">
            {form.photos.map((p, i) => (
              <span key={i} className="relative">
                <Photo src={p} title="Photo" className="w-14 h-14 object-cover rounded-md border border-[var(--leon-line)]" />
                <button type="button" onClick={() => removePhoto(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-4">✕</button>
              </span>
            ))}
            <ImagePicker url={null} onChange={addPhoto} size={56} />
          </div>
        </div>
        {form.outcome === 'Partial' && (
          <div className="pt-2 border-t border-[var(--leon-line)]">
            <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">Schedule Return Visit</p>
            <div className="grid grid-cols-3 gap-3">
              <Field label="Date"><TextInput type="date" value={form.returnDate} onChange={e => setForm({ ...form, returnDate: e.target.value })} /></Field>
              <Field label="Time"><TextInput type="time" value={form.returnTime} onChange={e => setForm({ ...form, returnTime: e.target.value })} /></Field>
              <Field label="Duration (min)"><TextInput type="number" value={form.returnDuration} onChange={e => setForm({ ...form, returnDuration: e.target.value })} /></Field>
            </div>
          </div>
        )}
      </div>
    </Modal>
  );
}

function DailyFieldReportsPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const sorted = [...project.dailyFieldReports].sort((a, b) => (a.date < b.date ? 1 : -1));
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Submit Daily Report</Button>}</div>
      <Collapsible id={`${project.id}-daily-field-reports`} title="Daily Field Reports" count={sorted.length}>
        {sorted.length === 0 ? <EmptyState text="No daily field reports yet." /> : (
          <div className="space-y-2">
            {sorted.map(r => {
              const scope = project.scopes.find(s => s.id === r.scopeId);
              return (
                <div key={r.id} className="border border-[var(--leon-line)] rounded-lg p-3 cursor-pointer" onClick={() => setDetailFor({ ...r, scopeName: scope ? scope.name : '' })}>
                  <div className="flex items-center justify-between gap-2 flex-wrap">
                    <p className="text-sm font-bold hover:underline">{fmtDate(r.date)} {scope ? `— ${scope.name}` : ''}</p>
                    <Badge tone="neutral">{r.manHours} man-hours</Badge>
                  </div>
                  <p className="text-xs text-[var(--leon-black)]/50">{r.crewCount} crew · {r.arrivalTime}–{r.departureTime} · Floors/Units: {r.floorsWorked || '—'}</p>
                  <p className="text-xs text-[var(--leon-black)]/50">Quantities installed: {r.quantitiesInstalled || '—'}</p>
                  {r.delays && <p className="text-xs text-[var(--leon-red)]">Delays/blockers: {r.delays}</p>}
                  {r.materialsIssues && <p className="text-xs text-[var(--leon-red)]">Materials missing/damaged: {r.materialsIssues}</p>}
                  {r.gcIssues && <p className="text-xs text-[var(--leon-black)]/50">GC/site issues: {r.gcIssues}</p>}
                  {r.planForTomorrow && <p className="text-xs text-[var(--leon-black)]/50">Plan for tomorrow: {r.planForTomorrow}</p>}
                </div>
              );
            })}
          </div>
        )}
      </Collapsible>
      <AddDailyFieldReportModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <RecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} title={`Daily Field Report — ${detailFor ? fmtDate(detailFor.date) : ''}`} printable
        fields={detailFor ? [
          { label: 'Scope', value: detailFor.scopeName }, { label: 'Crew Count', value: detailFor.crewCount }, { label: 'Man-Hours', value: detailFor.manHours },
          { label: 'Arrival / Departure', value: `${detailFor.arrivalTime}–${detailFor.departureTime}` }, { label: 'Floors/Units Worked', value: detailFor.floorsWorked },
          { label: 'Quantities Installed', value: detailFor.quantitiesInstalled }, { label: 'Delays/Blockers', value: detailFor.delays },
          { label: 'Materials Missing/Damaged', value: detailFor.materialsIssues }, { label: 'GC/Site Issues', value: detailFor.gcIssues },
          { label: 'Plan for Tomorrow', value: detailFor.planForTomorrow },
        ] : []}
        attachments={detailFor && detailFor.photos ? [{ name: 'Photo', url: detailFor.photos }] : []}
      />
    </div>
  );
}
function AddDailyFieldReportModal({ open, onClose, ctx, project }) {
  const empty = { date: todayISO(), scopeId: '', crewCount: '', arrivalTime: '07:00', departureTime: '15:30', hoursWorked: 8, floorsWorked: '', quantitiesInstalled: '', delays: '', materialsIssues: '', gcIssues: '', photos: null, planForTomorrow: '' };
  const [form, setForm] = useState(empty);
  useEffect(() => { if (open) setForm({ ...empty, scopeId: project.scopes[0]?.id || '' }); }, [open]);
  function submit() { if (!form.crewCount) return; ctx.addDailyFieldReport(project.id, { ...form, crewCount: Number(form.crewCount), hoursWorked: Number(form.hoursWorked), scopeId: form.scopeId || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Daily Field Report" wide footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Crew Count"><TextInput type="number" value={form.crewCount} onChange={e => setForm({ ...form, crewCount: e.target.value })} /></Field>
          <Field label="Arrival"><TextInput type="time" value={form.arrivalTime} onChange={e => setForm({ ...form, arrivalTime: e.target.value })} /></Field>
          <Field label="Departure"><TextInput type="time" value={form.departureTime} onChange={e => setForm({ ...form, departureTime: e.target.value })} /></Field>
        </div>
        <Field label="Hours Worked (per person)"><TextInput type="number" value={form.hoursWorked} onChange={e => setForm({ ...form, hoursWorked: e.target.value })} /></Field>
        <Field label="Floors/Units Worked"><TextInput value={form.floorsWorked} onChange={e => setForm({ ...form, floorsWorked: e.target.value })} /></Field>
        <Field label="Quantities Installed"><TextInput value={form.quantitiesInstalled} onChange={e => setForm({ ...form, quantitiesInstalled: e.target.value })} /></Field>
        <Field label="Delays/Blockers"><TextArea rows={2} value={form.delays} onChange={e => setForm({ ...form, delays: e.target.value })} /></Field>
        <Field label="Materials Missing/Damaged"><TextArea rows={2} value={form.materialsIssues} onChange={e => setForm({ ...form, materialsIssues: e.target.value })} /></Field>
        <Field label="GC/Site Issues"><TextArea rows={2} value={form.gcIssues} onChange={e => setForm({ ...form, gcIssues: e.target.value })} /></Field>
        <div className="flex items-center gap-3">
          <ImagePicker url={form.photos} onChange={url => setForm({ ...form, photos: url })} size={56} />
          <span className="text-xs text-[var(--leon-black)]/50">Photo</span>
        </div>
        <Field label="Plan for Tomorrow"><TextArea rows={2} value={form.planForTomorrow} onChange={e => setForm({ ...form, planForTomorrow: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function FieldIssuesPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const open = project.fieldIssues.filter(i => i.status === 'Open');
  const resolved = project.fieldIssues.filter(i => i.status !== 'Open');
  return (
    <div>
      {editable && (
        <div className="flex justify-center mb-4">
          <Button size="lg" variant="danger" onClick={() => setShowAdd(true)}>🚨 Report an Issue</Button>
        </div>
      )}
      <Collapsible title="Open" count={open.length}>
        {open.length === 0 ? <EmptyState text="No open field issues." /> : open.map(i => <FieldIssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailFor(i)} />)}
      </Collapsible>
      <Collapsible title="Resolved" count={resolved.length}>
        {resolved.length === 0 ? <EmptyState text="None yet." /> : resolved.map(i => <FieldIssueRow key={i.id} i={i} ctx={ctx} project={project} editable={editable} onOpen={() => setDetailFor(i)} />)}
      </Collapsible>
      <AddFieldIssueModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <FieldIssueDetailModal open={!!detailFor} issue={detailFor} onClose={() => setDetailFor(null)} project={project} />
    </div>
  );
}
function FieldIssueRow({ i, ctx, project, editable, onOpen }) {
  const scope = project.scopes.find(s => s.id === i.scopeId);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-1.5 cursor-pointer" onClick={onOpen}>
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="flex items-center gap-2">
          <Badge tone="black">{i.issueNumber}</Badge>
          <Badge tone={i.urgency === 'Critical' || i.urgency === 'High' ? 'red' : i.urgency === 'Medium' ? 'yellow' : 'neutral'}>{i.urgency}</Badge>
          {i.workStopped && <Badge tone="red">WORK STOPPED</Badge>}
        </div>
        {editable && i.status === 'Open' && <Button size="sm" variant="ghost" onClick={e => { e.stopPropagation(); ctx.setFieldIssueStatus(project.id, i.id, 'Resolved'); }}>Resolve</Button>}
      </div>
      <p className="text-sm font-semibold mt-1 hover:underline">{i.issueType} — {scope ? scope.name : ''}{i.location ? ` (${i.location})` : ''}</p>
      <p className="text-xs text-[var(--leon-black)]/50">{i.description}</p>
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">Raised {fmtDate(i.dateRaised)}</p>
      {i.photo && <ClickableImage src={i.photo} name="Field Issue Photo" className="w-16 h-16 object-cover rounded-md mt-1" />}
    </div>
  );
}
function FieldIssueDetailModal({ open, issue, onClose, project }) {
  if (!issue) return null;
  const scope = project.scopes.find(s => s.id === issue.scopeId);
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Field Issue ${issue.issueNumber || ''} — ${issue.issueType}`} printable
      fields={[
        { label: 'Status', value: issue.status }, { label: 'Urgency', value: issue.urgency },
        { label: 'Work Stopped', value: issue.workStopped ? 'Yes' : 'No' }, { label: 'Scope', value: scope ? scope.name : '—' },
        { label: 'Location', value: issue.location }, { label: 'Date Raised', value: fmtDate(issue.dateRaised) }, { label: 'Description', value: issue.description },
      ]}
      attachments={issue.photo ? [{ name: 'Photo', url: issue.photo }] : []}
    />
  );
}
function AddFieldIssueModal({ open, onClose, ctx, project, defaultScopeId }) {
  const empty = { scopeId: '', location: '', issueType: FIELD_ISSUE_TYPES[0], description: '', photo: null, urgency: 'Medium', workStopped: false };
  const [form, setForm] = useState(empty);
  useEffect(() => { if (open) setForm({ ...empty, scopeId: defaultScopeId || project.scopes[0]?.id || '' }); }, [open, defaultScopeId]);
  function submit() { if (!form.description.trim()) return; ctx.addFieldIssue(project.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Report an Issue" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit}>Report Issue</Button></>}>
      <div className="space-y-3">
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <Field label="Unit/Location"><TextInput value={form.location} onChange={e => setForm({ ...form, location: e.target.value })} placeholder="e.g. Floor 47, Unit 4702" /></Field>
        <Field label="Issue Type"><Select value={form.issueType} onChange={e => setForm({ ...form, issueType: e.target.value })}>{FIELD_ISSUE_TYPES.map(t => <option key={t}>{t}</option>)}</Select></Field>
        <Field label="Description"><TextArea rows={3} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <div className="flex items-center gap-3">
          <ImagePicker url={form.photo} onChange={url => setForm({ ...form, photo: url })} size={56} />
          <span className="text-xs text-[var(--leon-black)]/50">Photo</span>
        </div>
        <Field label="Urgency"><Select value={form.urgency} onChange={e => setForm({ ...form, urgency: e.target.value })}>{FIELD_ISSUE_URGENCY.map(u => <option key={u}>{u}</option>)}</Select></Field>
        <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={form.workStopped} onChange={e => setForm({ ...form, workStopped: e.target.checked })} /> Work Stopped?</label>
      </div>
    </Modal>
  );
}

function MaterialReceivingPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Log Receiving</Button>}</div>
      <Collapsible id={`${project.id}-material-receiving`} title="Material Receiving" count={project.materialReceipts.length}>
        {project.materialReceipts.length === 0 ? <EmptyState text="No material receiving logged yet." /> : (
          <div className="space-y-2">
            {project.materialReceipts.map(r => (
              <div key={r.id} className="border border-[var(--leon-line)] rounded-lg p-3 cursor-pointer" onClick={() => setDetailFor(r)}>
                <div className="flex items-center justify-between gap-2">
                  <p className="text-sm font-semibold hover:underline">{r.description}</p>
                  <StatusBadge status={r.outcome} />
                </div>
                <p className="text-xs text-[var(--leon-black)]/50">{fmtDate(r.date)}{r.note ? ` — ${r.note}` : ''}</p>
                {r.photos && <ClickableImage src={r.photos} name="Receipt Photo" className="w-16 h-16 object-cover rounded-md mt-1" />}
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <AddMaterialReceiptModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <RecordDetailModal open={!!detailFor} onClose={() => setDetailFor(null)} title={`Material Receiving — ${detailFor?.description || ''}`} printable
        fields={detailFor ? [
          { label: 'Outcome', value: detailFor.outcome }, { label: 'Date', value: fmtDate(detailFor.date) },
          { label: 'Related PO', value: (project.purchaseOrders.find(po => po.id === detailFor.poId) || {}).poNumber }, { label: 'Note', value: detailFor.note },
        ] : []}
        attachments={detailFor && detailFor.photos ? [{ name: 'Photo', url: detailFor.photos }] : []}
      />
    </div>
  );
}
function AddMaterialReceiptModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ poId: '', description: '', outcome: 'Received', photos: null, note: '' });
  useEffect(() => { if (open) setForm({ poId: '', description: '', outcome: 'Received', photos: null, note: '' }); }, [open]);
  const needsPhoto = form.outcome === 'Damaged' || form.outcome === 'Wrong Item';
  function submit() {
    if (!form.description.trim()) return;
    if (needsPhoto && !form.photos) { window.alert('A photo is required before closing a damaged/wrong-item receiving report.'); return; }
    ctx.addMaterialReceipt(project.id, form);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title="Log Material Receiving" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Log Receiving</Button></>}>
      <div className="space-y-3">
        <Field label="Shipment / PO"><Select value={form.poId} onChange={e => setForm({ ...form, poId: e.target.value })}><option value="">— none —</option>{project.purchaseOrders.map(po => <option key={po.id} value={po.id}>{po.vendorName} — {fmtMoney(po.amount)}</option>)}</Select></Field>
        <Field label="Description"><TextInput value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <Field label="Outcome"><Select value={form.outcome} onChange={e => setForm({ ...form, outcome: e.target.value })}>{MATERIAL_RECEIVING_OUTCOMES.map(o => <option key={o}>{o}</option>)}</Select></Field>
        <div className="flex items-center gap-3">
          <ImagePicker url={form.photos} onChange={url => setForm({ ...form, photos: url })} size={56} />
          <span className="text-xs text-[var(--leon-black)]/50">{needsPhoto ? 'Photo required for damaged/wrong item' : 'Photo (optional)'}</span>
        </div>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function punchResponseTone(status) {
  if (status === 'Completed') return 'green';
  if (status === 'Not Done' || status === 'Additional Material Needed') return 'red';
  if (status === 'Pending') return 'yellow';
  return 'neutral';
}
function PunchListPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const canClose = ['Admin', 'Accounting', 'General Manager', 'Project Coordinator'].includes(ctx.currentRole);
  const [showAdd, setShowAdd] = useState(false);
  const [respondFor, setRespondFor] = useState(null);
  const [detailFor, setDetailFor] = useState(null);
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ Add Punch Item</Button>}</div>
      <Collapsible id={`${project.id}-punch-list`} title="Punch List" count={project.punchItems.length}>
      {project.punchItems.length === 0 ? <EmptyState text="No punch items yet." /> : (
        <div className="space-y-2">
          {project.punchItems.map(p => (
            <div key={p.id} className="border border-[var(--leon-line)] rounded-lg p-3">
              <div className="flex items-center justify-between gap-2 flex-wrap cursor-pointer" onClick={() => setDetailFor(p)}>
                <p className="text-sm font-semibold hover:underline">{[p.building, p.floor, p.unit, p.room, p.item].filter(Boolean).join(' · ')}</p>
                <div className="flex items-center gap-2">
                  <Badge tone={p.priority === 'High' ? 'red' : p.priority === 'Medium' ? 'yellow' : 'neutral'}>{p.priority}</Badge>
                  <StatusBadge status={p.status} />
                </div>
              </div>
              <p className="text-xs text-[var(--leon-black)]/50">{p.problem} · Responsible: {p.responsibleParty || '—'} · Target {fmtDate(p.targetDate)}</p>
              <div className="flex items-center gap-2 mt-1">
                {p.photoBefore && <ClickableImage src={p.photoBefore} name="Punch Item — Before" className="w-14 h-14 object-cover rounded-md" />}
                {p.photoAfter && <ClickableImage src={p.photoAfter} name="Punch Item — After" className="w-14 h-14 object-cover rounded-md" />}
              </div>

              {p.responseStatus && (
                <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
                  <div className="flex items-center gap-1.5 flex-wrap">
                    <Badge tone={punchResponseTone(p.responseStatus)}>{p.responseStatus}</Badge>
                    <span className="text-[11px] text-[var(--leon-black)]/40">by {p.respondedBy}, {fmtDate(p.respondedDate)}{p.repairCompletedDate ? ` · repaired ${fmtDate(p.repairCompletedDate)}` : ''}</span>
                  </div>
                  {p.responseNotes && <p className="text-xs text-[var(--leon-black)]/60 mt-1">{p.responseNotes}</p>}
                  {p.responsePhotos.length > 0 && (
                    <div className="flex flex-wrap gap-2 mt-1.5">
                      {p.responsePhotos.map(ph => <FileField key={ph.id} name={ph.file} url={ph.fileUrl} editable={false} onChange={() => {}} />)}
                    </div>
                  )}
                </div>
              )}

              {editable && p.status !== 'Closed' && (
                <div className="flex gap-2 mt-2 items-center flex-wrap">
                  <Button size="sm" onClick={() => setRespondFor(p)}>Respond</Button>
                  {p.status === 'Completed – Awaiting Verification' && canClose && <Button size="sm" variant="black" onClick={() => ctx.setPunchStatus(project.id, p.id, 'Closed')}>Verify &amp; Close</Button>}
                  {p.status === 'Completed – Awaiting Verification' && !canClose && <span className="text-[11px] text-[var(--leon-black)]/40 italic">Awaiting PM/supervisor verification to close.</span>}
                </div>
              )}
            </div>
          ))}
        </div>
      )}
      </Collapsible>
      <AddPunchItemModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
      <RespondPunchItemModal open={!!respondFor} item={respondFor} onClose={() => setRespondFor(null)} ctx={ctx} project={project} />
      <PunchItemDetailModal open={!!detailFor} item={detailFor} onClose={() => setDetailFor(null)} />
    </div>
  );
}
function PunchItemDetailModal({ open, item, onClose }) {
  if (!item) return null;
  const history = [{ id: 'created', date: item.dateRaised || item.createdDate, user: item.createdBy || 'Field Team', reason: 'Punch item created' }];
  if (item.responseStatus) history.push({ id: 'response', date: item.respondedDate, user: item.respondedBy, reason: `Response — ${item.responseStatus}`, notes: item.responseNotes });
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Punch Item — ${item.item || [item.building, item.floor, item.unit, item.room].filter(Boolean).join(' · ')}`} printable
      fields={[
        { label: 'Status', value: item.status }, { label: 'Priority', value: item.priority },
        { label: 'Location', value: [item.building, item.floor, item.unit, item.room].filter(Boolean).join(' · ') },
        { label: 'Problem', value: item.problem }, { label: 'Responsible Party', value: item.responsibleParty },
        { label: 'Target Date', value: fmtDate(item.targetDate) }, { label: 'Response Status', value: item.responseStatus },
        { label: 'Repair Completed', value: item.repairCompletedDate ? fmtDate(item.repairCompletedDate) : '—' },
      ]}
      attachments={[
        ...(item.photoBefore ? [{ name: 'Before', url: item.photoBefore }] : []),
        ...(item.photoAfter ? [{ name: 'After', url: item.photoAfter }] : []),
        ...(item.responsePhotos || []).map(ph => ({ name: ph.file, url: ph.fileUrl })),
      ]}
      history={history}
    />
  );
}
function RespondPunchItemModal({ open, item, onClose, ctx, project }) {
  const blank = { responseStatus: PUNCH_RESPONSE_STATUSES[0], responseNotes: '', repairCompletedDate: todayISO() };
  const [form, setForm] = useState(blank);
  const [newPhotos, setNewPhotos] = useState([]);
  useEffect(() => { if (open) { setForm(blank); setNewPhotos([]); } }, [open, item]);
  if (!item) return null;
  function addPhoto(fname, url) { setNewPhotos(p => [...p, makePunchPhoto(fname, url)]); }
  function submit() {
    ctx.respondToPunchItem(project.id, item.id, { ...form, repairCompletedDate: form.responseStatus === 'Completed' ? form.repairCompletedDate : null, newPhotos });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Respond — ${item.item || 'Punch Item'}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit Response</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">{item.problem}</p>
        <Field label="Response Status">
          <Select value={form.responseStatus} onChange={e => setForm({ ...form, responseStatus: e.target.value })}>
            {PUNCH_RESPONSE_STATUSES.map(s => <option key={s}>{s}</option>)}
          </Select>
        </Field>
        {form.responseStatus === 'Completed' && (
          <Field label="Repair Completed Date"><TextInput type="date" value={form.repairCompletedDate} onChange={e => setForm({ ...form, repairCompletedDate: e.target.value })} /></Field>
        )}
        <Field label="Notes"><TextArea rows={3} value={form.responseNotes} onChange={e => setForm({ ...form, responseNotes: e.target.value })} placeholder="What was done, what's blocking it, materials needed, etc." /></Field>
        <Field label="Photos of Item">
          <div className="space-y-1.5">
            {newPhotos.map(ph => <div key={ph.id}><FileField name={ph.file} url={ph.fileUrl} editable={false} onChange={() => {}} /></div>)}
            <FileField name="" url={null} placeholder="Add a photo" editable onChange={addPhoto} />
          </div>
        </Field>
      </div>
    </Modal>
  );
}
function AddPunchItemModal({ open, onClose, ctx, project, defaultScopeId }) {
  const empty = { scopeId: '', building: '', floor: '', unit: '', room: '', item: '', problem: '', responsibleParty: '', assignedSubcontractorId: '', priority: 'Medium', photoBefore: null, targetDate: todayISO() };
  const [form, setForm] = useState(empty);
  useEffect(() => { if (open) setForm({ ...empty, scopeId: defaultScopeId || project.scopes[0]?.id || '' }); }, [open, defaultScopeId]);
  function submit() { if (!form.problem.trim()) return; ctx.addPunchItem(project.id, { ...form, assignedSubcontractorId: form.assignedSubcontractorId || null }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Punch Item" wide footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Punch Item</Button></>}>
      <div className="space-y-3">
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Building"><TextInput value={form.building} onChange={e => setForm({ ...form, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Unit"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
          <Field label="Room"><TextInput value={form.room} onChange={e => setForm({ ...form, room: e.target.value })} /></Field>
        </div>
        <Field label="Item"><TextInput value={form.item} onChange={e => setForm({ ...form, item: e.target.value })} /></Field>
        <Field label="Problem"><TextArea rows={2} value={form.problem} onChange={e => setForm({ ...form, problem: e.target.value })} /></Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Responsible Party (notes)"><TextInput value={form.responsibleParty} onChange={e => setForm({ ...form, responsibleParty: e.target.value })} /></Field>
          <Field label="Priority"><Select value={form.priority} onChange={e => setForm({ ...form, priority: e.target.value })}>{PUNCH_PRIORITIES.map(p => <option key={p}>{p}</option>)}</Select></Field>
        </div>
        <Field label="Assigned Subcontractor">
          <Select value={form.assignedSubcontractorId} onChange={e => setForm({ ...form, assignedSubcontractorId: e.target.value })}>
            <option value="">— none —</option>
            {ctx.subcontractors.filter(s => s.status === 'Active').map(s => <option key={s.id} value={s.id}>{s.companyName}</option>)}
          </Select>
        </Field>
        <Field label="Target Date"><TextInput type="date" value={form.targetDate} onChange={e => setForm({ ...form, targetDate: e.target.value })} /></Field>
        <div className="flex items-center gap-3">
          <ImagePicker url={form.photoBefore} onChange={url => setForm({ ...form, photoBefore: url })} size={56} />
          <span className="text-xs text-[var(--leon-black)]/50">Photo Before</span>
        </div>
      </div>
    </Modal>
  );
}

// ---- Field Measurement Report (Installation § field measurement request) --
// One thread per scope+location; every site visit appends a new revision —
// prior measurements/notes/photos are never overwritten.
function fieldMeasurementTone(status) {
  if (status === 'Field Condition Requires Review') return 'red';
  if (['Completed', 'Approved for Production', 'Approved for Installation'].includes(status)) return 'green';
  if (['Issue Found', 'Revision Required'].includes(status)) return 'red';
  if (['Measurement Scheduled', 'Measurement in Progress', 'Awaiting Clarification'].includes(status)) return 'yellow';
  return 'neutral';
}
function FieldMeasurementPanel({ ctx, project }) {
  const editable = ctx.canEdit('installation');
  const [showAdd, setShowAdd] = useState(false);
  return (
    <div>
      <div className="flex justify-end mb-2">{editable && <Button size="sm" onClick={() => setShowAdd(true)}>+ New Field Measurement Report</Button>}</div>
      <Collapsible id={`${project.id}-field-measurements`} title="Field Measurement Report" count={project.fieldMeasurements.length}>
        {project.fieldMeasurements.length === 0 ? <EmptyState text="No field measurement reports yet." /> : (
          <div className="space-y-2">
            {project.fieldMeasurements.map(t => <FieldMeasurementThreadCard key={t.id} ctx={ctx} project={project} thread={t} editable={editable} />)}
          </div>
        )}
      </Collapsible>
      <AddFieldMeasurementThreadModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
    </div>
  );
}
function AddFieldMeasurementThreadModal({ open, onClose, ctx, project, defaultScopeId }) {
  const empty = { scopeId: '', building: '', floor: '', unit: '', room: '' };
  const [form, setForm] = useState(empty);
  useEffect(() => { if (open) setForm({ ...empty, scopeId: defaultScopeId || project.scopes[0]?.id || '' }); }, [open, defaultScopeId]);
  function submit() { ctx.addFieldMeasurementThread(project.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="New Field Measurement Report" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create</Button></>}>
      <div className="space-y-3">
        <Field label="Scope"><Select value={form.scopeId} onChange={e => setForm({ ...form, scopeId: e.target.value })}>{project.scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}</Select></Field>
        <div className="grid grid-cols-4 gap-3">
          <Field label="Building"><TextInput value={form.building} onChange={e => setForm({ ...form, building: e.target.value })} /></Field>
          <Field label="Floor"><TextInput value={form.floor} onChange={e => setForm({ ...form, floor: e.target.value })} /></Field>
          <Field label="Unit/Area"><TextInput value={form.unit} onChange={e => setForm({ ...form, unit: e.target.value })} /></Field>
          <Field label="Room"><TextInput value={form.room} onChange={e => setForm({ ...form, room: e.target.value })} /></Field>
        </div>
      </div>
    </Modal>
  );
}
function FieldMeasurementThreadCard({ ctx, project, thread, editable }) {
  const [revModal, setRevModal] = useState(false);
  const [compareIds, setCompareIds] = useState([]);
  const scope = project.scopes.find(s => s.id === thread.scopeId);
  const sorted = [...thread.revisions].sort((a, b) => b.revisionNumber - a.revisionNumber);
  const compareRevs = compareIds.map(id => thread.revisions.find(r => r.id === id)).filter(Boolean);
  const [detailOpen, setDetailOpen] = useState(false);
  function toggleCompare(id) {
    setCompareIds(ids => ids.includes(id) ? ids.filter(x => x !== id) : ids.length < 2 ? [...ids, id] : [ids[1], id]);
  }
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="cursor-pointer" onClick={() => setDetailOpen(true)}>
          <p className="text-sm font-bold hover:underline">{[thread.building, thread.floor, thread.unit, thread.room].filter(Boolean).join(' · ') || 'Field Measurement'}</p>
          <p className="text-xs text-[var(--leon-black)]/50">{scope ? scope.name : '—'} · Created {fmtDate(thread.createdDate)} by {thread.createdBy}</p>
        </div>
        <div className="flex items-center gap-2">
          <Badge tone={fieldMeasurementTone(thread.status)}>{thread.status}</Badge>
          {editable && <Button size="sm" variant="ghost" onClick={() => setRevModal(true)}>+ Add Revision</Button>}
        </div>
      </div>
      {thread.status === 'Field Condition Requires Review' && (
        <p className="mt-2 text-xs font-semibold text-[var(--leon-red)]">⚠ Field condition requires review — flagged for the project/design team before production or installation continues.</p>
      )}
      {compareRevs.length === 2 && (
        <div className="mt-2 border border-[var(--leon-line)] rounded-lg overflow-hidden">
          <table className="w-full text-[11px]">
            <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-[var(--leon-black)]/50 uppercase"><th className="px-2 py-1">Measurement</th><th className="px-2 py-1">Rev. {compareRevs[0].revisionNumber}</th><th className="px-2 py-1">Rev. {compareRevs[1].revisionNumber}</th></tr></thead>
            <tbody>
              {[...new Set([...compareRevs[0].measurements, ...compareRevs[1].measurements].map(m => m.label))].map(label => (
                <tr key={label} className="border-t border-[var(--leon-line)]">
                  <td className="px-2 py-1 font-semibold">{label}</td>
                  <td className="px-2 py-1">{(compareRevs[0].measurements.find(m => m.label === label) || {}).value || '—'}</td>
                  <td className="px-2 py-1">{(compareRevs[1].measurements.find(m => m.label === label) || {}).value || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <div className="mt-2 space-y-2">
        {sorted.map(r => (
          <div key={r.id} className="text-[11px] border-t border-[var(--leon-line)] pt-2 first:border-t-0 first:pt-0">
            <div className="flex items-center gap-1.5 flex-wrap">
              <label className="flex items-center gap-1"><input type="checkbox" checked={compareIds.includes(r.id)} onChange={() => toggleCompare(r.id)} /> Compare</label>
              <Badge tone={fieldMeasurementTone(r.status)}>Rev. {r.revisionNumber} · {r.status}</Badge>
              <span className="text-[var(--leon-black)]/40">Measured {fmtDate(r.measurementDate)} (visit {fmtDate(r.siteVisitDate)}) · {r.installer} · {r.unitSystem}</span>
              {r.basis && <Badge tone={r.basis === 'Finished Walls' ? 'green' : r.basis === 'Rough Opening' ? 'yellow' : 'neutral'}>{r.basis}</Badge>}
            </div>
            {r.measurements.length > 0 && (
              <div className="mt-1 grid sm:grid-cols-3 gap-x-3 gap-y-0.5">
                {r.measurements.map(m => <span key={m.id}>{m.label}: <strong>{m.value}</strong></span>)}
              </div>
            )}
            {r.notes && <p className="text-[var(--leon-black)]/60 mt-1">{r.notes}</p>}
            {(r.photos.length > 0 || r.attachments.length > 0) && (
              <div className="flex flex-wrap gap-2 mt-1">
                {r.photos.map(p => <Photo key={p.id} src={p.fileUrl} title={p.file || 'Photo'} className="w-14 h-14 object-cover rounded-md" />)}
                {r.attachments.map(a => <FileField key={a.id} name={a.file} url={a.fileUrl} editable={false} onChange={() => {}} />)}
              </div>
            )}
            {r.fieldNotes.length > 0 && (
              <div className="mt-1.5 space-y-1">
                {r.fieldNotes.map(n => (
                  <div key={n.id} className="border-l-2 border-[var(--leon-red)] pl-2">
                    <p className="flex items-center gap-1.5 flex-wrap"><Badge tone={n.priority === 'High' ? 'red' : n.priority === 'Medium' ? 'yellow' : 'neutral'}>{n.priority}</Badge><strong>{n.issueCategory}</strong></p>
                    <p className="text-[var(--leon-black)]/60">{n.note}{n.responsibleParty ? ` — Responsible: ${n.responsibleParty}` : ''}</p>
                    <p className="text-[var(--leon-black)]/40">{fmtDate(n.date)} · {n.createdBy}</p>
                    {n.photo && <ClickableImage src={n.photo} name="Field Note Photo" className="w-12 h-12 object-cover rounded-md mt-0.5" />}
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}
      </div>
      <AddFieldMeasurementRevisionModal open={revModal} thread={thread} onClose={() => setRevModal(false)} ctx={ctx} project={project} />
      <RecordDetailModal open={detailOpen} onClose={() => setDetailOpen(false)}
        title={`Field Measurement — ${[thread.building, thread.floor, thread.unit, thread.room].filter(Boolean).join(' · ') || 'Report'}`}
        printable
        fields={[
          { label: 'Project', value: project.name }, { label: 'Scope', value: scope ? scope.name : '—' },
          { label: 'Status', value: thread.status }, { label: 'Created By', value: thread.createdBy }, { label: 'Created Date', value: fmtDate(thread.createdDate) },
          { label: 'Latest Revision', value: sorted[0] ? `Rev. ${sorted[0].revisionNumber} — ${fmtDate(sorted[0].measurementDate)} by ${sorted[0].installer}` : '—' },
        ]}
        attachments={sorted.flatMap(r => [...r.photos.map(p => ({ name: `Rev ${r.revisionNumber} Photo`, url: p.fileUrl })), ...r.attachments.map(a => ({ name: a.file, url: a.fileUrl }))])}
        history={sorted.map(r => ({ id: r.id, date: r.measurementDate, user: r.installer, reason: `Rev. ${r.revisionNumber} — ${r.status}`, notes: r.notes }))}
      />
    </div>
  );
}
function AddFieldMeasurementRevisionModal({ open, thread, onClose, ctx, project }) {
  const blankMeasurement = { label: FIELD_MEASUREMENT_LABELS[0], value: '' };
  const blankNote = { note: '', responsibleParty: '', issueCategory: FIELD_NOTE_CATEGORIES[0], priority: 'Medium', photo: null };
  const blank = { measurementDate: todayISO(), siteVisitDate: todayISO(), installer: ctx.currentUserName, status: FIELD_MEASUREMENT_STATUSES[0], unitSystem: 'Imperial', basis: 'Rough Opening', basisNote: '', notes: '' };
  const [form, setForm] = useState(blank);
  const [measurements, setMeasurements] = useState([]);
  const [attachments, setAttachments] = useState([]);
  const [photos, setPhotos] = useState([]);
  const [fieldNotes, setFieldNotes] = useState([]);
  useEffect(() => { if (open) { setForm(blank); setMeasurements([]); setAttachments([]); setPhotos([]); setFieldNotes([]); } }, [open]);
  if (!thread) return null;
  function submit() {
    ctx.addFieldMeasurementRevision(project.id, thread.id, {
      ...form,
      measurements: measurements.filter(m => m.value).map(m => ({ id: uid('meas'), ...m })),
      attachments, photos: photos.map(p => ({ id: uid('fmphoto'), ...p })),
      fieldNotes: fieldNotes.filter(n => n.note.trim()).map(n => ({ id: uid('fnote'), date: todayISO(), createdBy: ctx.currentUserName, ...n })),
    });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Add Revision — Rev. ${thread.revisions.length}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Revision</Button></>}>
      <div className="space-y-4">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Measurement Date"><TextInput type="date" value={form.measurementDate} onChange={e => setForm({ ...form, measurementDate: e.target.value })} /></Field>
          <Field label="Site Visit Date"><TextInput type="date" value={form.siteVisitDate} onChange={e => setForm({ ...form, siteVisitDate: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Installer / Field Person"><TextInput value={form.installer} onChange={e => setForm({ ...form, installer: e.target.value })} /></Field>
          <Field label="Unit System"><Select value={form.unitSystem} onChange={e => setForm({ ...form, unitSystem: e.target.value })}>{DIMENSION_UNITS.map(u => <option key={u}>{u}</option>)}</Select></Field>
          <Field label="Status">
            <Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
              {FIELD_MEASUREMENT_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
        </div>
        {/* Rough opening vs finished walls decides whether a finish allowance
            still has to come off these numbers. Getting it wrong is a
            re-fabrication, so it is asked explicitly rather than assumed. */}
        <Field label="Measured against" hint={(MEASUREMENT_BASIS.find(b => b.key === form.basis) || {}).hint}>
          <div className="flex gap-2 flex-wrap">
            {MEASUREMENT_BASIS.map(b => (
              <button key={b.key} type="button" onClick={() => setForm({ ...form, basis: b.key })}
                className={`px-3 py-2 rounded-lg text-xs font-semibold border transition ${form.basis === b.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:border-[var(--leon-brown-light)]'}`}>
                {b.key}
              </button>
            ))}
          </div>
        </Field>
        {form.basis === 'Mixed / See Notes' && (
          <Field label="Which openings were measured how?">
            <TextInput value={form.basisNote} onChange={e => setForm({ ...form, basisNote: e.target.value })} placeholder="e.g. Units 1–4 rough opening, Unit 5 finished" />
          </Field>
        )}
        {form.status === 'Field Condition Requires Review' && <p className="text-xs font-semibold text-[var(--leon-red)]">⚠ This will flag the report for project/design team review.</p>}

        <Field label="Measurements">
          <div className="space-y-1.5">
            {measurements.map((m, i) => (
              <div key={i} className="flex items-center gap-2">
                <Select value={m.label} onChange={e => setMeasurements(ms => ms.map((x, xi) => xi === i ? { ...x, label: e.target.value } : x))} className="!w-52">
                  {FIELD_MEASUREMENT_LABELS.map(l => <option key={l}>{l}</option>)}
                </Select>
                <TextInput value={m.value} onChange={e => setMeasurements(ms => ms.map((x, xi) => xi === i ? { ...x, value: e.target.value } : x))} placeholder={form.unitSystem === 'Metric' ? 'e.g. 61 cm' : 'e.g. 24"'} />
                <IconBtn title="Remove" onClick={() => setMeasurements(ms => ms.filter((_, xi) => xi !== i))}>✕</IconBtn>
              </div>
            ))}
            <Button size="sm" variant="ghost" onClick={() => setMeasurements(ms => [...ms, { ...blankMeasurement }])}>+ Add Measurement</Button>
          </div>
        </Field>

        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>

        <Field label="Attachments">
          <div className="space-y-1.5">
            {attachments.map((a, i) => <div key={i}><FileField name={a.file} url={a.fileUrl} editable={false} onChange={() => {}} /></div>)}
            <FileField name="" url={null} placeholder="Add an attachment" editable onChange={(fname, url) => setAttachments(a => [...a, { file: fname, fileUrl: url }])} />
          </div>
        </Field>
        <Field label="Photos">
          <div className="flex items-center gap-2 flex-wrap">
            {photos.map((p, i) => <Photo key={i} src={p.fileUrl} title={p.file || 'Photo'} className="w-14 h-14 object-cover rounded-md" />)}
            <ImagePicker url={null} onChange={url => setPhotos(p => [...p, { file: 'photo', fileUrl: url }])} size={56} />
          </div>
        </Field>

        <Field label="Field Notes">
          <div className="space-y-2">
            {fieldNotes.map((n, i) => (
              <div key={i} className="border border-[var(--leon-line)] rounded-lg p-2 space-y-1.5">
                <div className="flex items-center justify-between">
                  <Select value={n.issueCategory} onChange={e => setFieldNotes(ns => ns.map((x, xi) => xi === i ? { ...x, issueCategory: e.target.value } : x))} className="!w-64 !py-1 !text-xs">
                    {FIELD_NOTE_CATEGORIES.map(c => <option key={c}>{c}</option>)}
                  </Select>
                  <IconBtn title="Remove note" onClick={() => setFieldNotes(ns => ns.filter((_, xi) => xi !== i))}>✕</IconBtn>
                </div>
                <TextArea rows={2} value={n.note} onChange={e => setFieldNotes(ns => ns.map((x, xi) => xi === i ? { ...x, note: e.target.value } : x))} placeholder="Describe the field note…" />
                <div className="grid grid-cols-2 gap-2">
                  <TextInput value={n.responsibleParty} onChange={e => setFieldNotes(ns => ns.map((x, xi) => xi === i ? { ...x, responsibleParty: e.target.value } : x))} placeholder="Responsible party (if known)" className="!py-1 !text-xs" />
                  <Select value={n.priority} onChange={e => setFieldNotes(ns => ns.map((x, xi) => xi === i ? { ...x, priority: e.target.value } : x))} className="!py-1 !text-xs">
                    {FIELD_NOTE_PRIORITIES.map(p => <option key={p}>{p}</option>)}
                  </Select>
                </div>
                <ImagePicker url={n.photo} onChange={url => setFieldNotes(ns => ns.map((x, xi) => xi === i ? { ...x, photo: url } : x))} size={44} />
              </div>
            ))}
            <Button size="sm" variant="ghost" onClick={() => setFieldNotes(ns => [...ns, { ...blankNote }])}>+ Add Field Note</Button>
          </div>
        </Field>
      </div>
    </Modal>
  );
}

// ---- Sales (Quotes & Contracts hub: Overview / Quotes / Profitability / Follow-Ups / Contract / Change Orders / Back-charges) ----
const SALES_SUBTABS = [
  { key: 'overview', label: 'Overview', icon: '📊' },
  // Pricing a bid off a take-off. Interiors only for now, and deliberately
  // isolated — see the Quote Analysis mutators in App().
  { key: 'quoteDraft', label: 'Quote Analysis', icon: '🧮' },
  { key: 'quotes', label: 'Quotes', icon: '🧾' },
  // The bill of quantities for this job. It is the quotation for a client whose
  // region quotes that way, so it belongs beside the other quote tabs and not
  // only on the company-wide Sales screen.
  { key: 'boq', label: 'Bill of Quantities', icon: '🧾' },
  { key: 'profitability', label: 'Profitability', icon: '📈' },
  { key: 'followUps', label: 'Follow-Ups', icon: '🔔' },
  { key: 'contract', label: 'Contract', icon: '📜' },
  { key: 'changeOrders', label: 'Change Orders', icon: '🔁' },
  { key: 'backCharges', label: 'Back-charges', icon: '↩️' },
  // Everything this job is costing us, and the invoices behind it.
  { key: 'jobCosts', label: 'Job Costs', icon: '💵' },
];
function SalesTab({ ctx, project, pendingNav }) {
  // This hub is only reachable with financial access OR by being the
  // salesperson assigned to this job. Without financial access you are here for
  // exactly one reason — your job's costs and the subcontractor invoices
  // waiting on you — so that is all you get. Gate on canSeeFin, NOT on the
  // quotes module: several roles keep a default 'view' on quotes, and using
  // that here handed an assigned coordinator the Profitability tab.
  const salesOnly = !ctx.canSeeFin;
  const subtabs = salesOnly ? SALES_SUBTABS.filter(t => t.key === 'jobCosts') : SALES_SUBTABS;
  const [sub, setSub] = useState(() => (ctx.bootQuoteId ? 'quoteDraft' : null) || (pendingNav && pendingNav.subtab) || (salesOnly ? 'jobCosts' : 'overview'));
  useEffect(() => { if (pendingNav && pendingNav.subtab) setSub(pendingNav.subtab); }, [pendingNav]);
  // Never leave a sales-only viewer sitting on a subtab that is not in their
  // list — a stale value would render a panel their tab bar does not show.
  useEffect(() => { if (salesOnly && sub !== 'jobCosts') setSub('jobCosts'); }, [salesOnly, sub]);
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {subtabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
        ))}
      </div>
      <HubTools />
      {sub === 'overview' && <>
        <div className="mb-4"><ProjectTaxPanel ctx={ctx} project={project} /></div>
        <SalesOverviewSubTab ctx={ctx} project={project} />
      </>}
      {sub === 'quotes' && <SalesQuotesSubTab ctx={ctx} project={project} />}
      {sub === 'boq' && (typeof BoqSection === 'function'
        ? <BoqSection ctx={ctx} projectId={project.id} onProject={() => {}} />
        : <EmptyState text="The bill-of-quantities module (softwares/boq.jsx) is not loaded." />)}
      {sub === 'profitability' && <ProfitabilityTab ctx={ctx} project={project} />}
      {sub === 'followUps' && <FollowUpsSubTab ctx={ctx} project={project} />}
      {sub === 'contract' && <ContractSubTab ctx={ctx} project={project} />}
      {sub === 'changeOrders' && <ChangeOrdersSubTab ctx={ctx} project={project} />}
      {sub === 'backCharges' && <BackChargesSubTab ctx={ctx} project={project} />}
      {sub === 'jobCosts' && <SalesJobCostsSubTab ctx={ctx} project={project} />}
      {sub === 'quoteDraft' && <QuoteDraftSubTab ctx={ctx} project={project} />}
    </div>
  );
}

// ══════════════════════════════════════════════ Project Closeout
// Finishing a job is its own piece of work, not just the last stage turning
// green: things that must be true before it can be closed, documents the client
// is owed, photographs worth keeping, a warranty clock that starts, and what
// the job taught us. All of it lives on the project.
function CloseoutTab({ ctx, project }) {
  const co = project.closeout || makeCloseout();
  const editable = ctx.canEdit('closeout');
  const [sub, setSub] = useState('checklist');
  const prog = closeoutProgress(co);
  const closed = co.status === 'Closed';
  const set = f => ctx.updateCloseout(project.id, f);
  const tabs = [
    { key: 'checklist', label: 'Checklist', icon: '✅', n: prog.open },
    { key: 'photos', label: 'Finished Photos', icon: '📸', n: co.photos.length },
    { key: 'documents', label: 'Handover Documents', icon: '📁', n: co.documents.length },
    { key: 'lessons', label: 'Lessons Learned', icon: '💡', n: co.lessons.length },
    { key: 'summary', label: 'Summary & Sign-Off', icon: '🏁' },
  ];

  return (
    <div className="space-y-4" data-print-region="Closeout">
      {/* Where the job stands, in one strip */}
      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
        <div className="p-4 flex items-center gap-4 flex-wrap">
          <div className="flex-1 min-w-[240px]">
            <div className="flex items-baseline gap-2 mb-1">
              <span className="text-lg font-bold">Closeout</span>
              <Badge tone={closed ? 'green' : co.status === 'Ready to Close' ? 'brown' : 'neutral'}>{co.status}</Badge>
            </div>
            <div className="h-2 rounded-full bg-[var(--leon-line)] overflow-hidden">
              <div className="h-full bg-[var(--leon-brown)] transition-all"
                style={{ width: `${Math.round(prog.pct * 100)}%` }} />
            </div>
            <div className="text-xs text-[var(--leon-black)]/55 mt-1">
              {prog.done} of {prog.total} settled
              {prog.open ? ` · ${prog.open} still open` : ' · nothing outstanding'}
            </div>
          </div>
          <div className="flex items-end gap-3 flex-wrap">
            <Field label="Substantial completion">
              <TextInput type="date" disabled={!editable || closed} value={co.substantialCompletionDate || ''}
                onChange={e => set({ substantialCompletionDate: e.target.value || null,
                  // The warranty clock starts here unless someone has already
                  // set it to something else on purpose.
                  ...(co.warrantyStartDate ? {} : { warrantyStartDate: e.target.value || null }) })} />
            </Field>
            <Field label="Status">
              <Select disabled={!editable} value={co.status} onChange={e => set({ status: e.target.value })}>
                {CLOSEOUT_STATUSES.map(x => <option key={x}>{x}</option>)}
              </Select>
            </Field>
            {editable && (closed
              ? <Button variant="ghost" onClick={() => ctx.reopenCloseout(project.id)}>Reopen</Button>
              : <Button disabled={prog.open > 0}
                  title={prog.open ? `${prog.open} checklist item${prog.open === 1 ? '' : 's'} still open` : 'Close this project'}
                  onClick={() => { if (confirm('Close this project out?')) ctx.closeProject(project.id); }}>
                  🏁 Close the project
                </Button>)}
          </div>
        </div>
        {closed && (
          <div className="px-4 py-2 bg-[var(--leon-cream)] border-t border-[var(--leon-line)] text-xs">
            Closed {fmtDate(co.closedDate)} by {co.closedBy || '—'}
            {warrantyEndDate(co) && <> · warranty runs to <b>{fmtDate(warrantyEndDate(co))}</b></>}
          </div>
        )}
      </div>

      <div className="flex gap-1 border-b border-[var(--leon-line)] flex-wrap">
        {tabs.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>{t.label}
            {t.n ? <span className="ml-1.5 opacity-50">{t.n}</span> : null}
          </button>
        ))}
      </div>

      {sub === 'checklist' && <CloseoutChecklist ctx={ctx} project={project} co={co} editable={editable && !closed} />}
      {sub === 'photos' && <CloseoutPhotos ctx={ctx} project={project} co={co} editable={editable} />}
      {sub === 'documents' && <CloseoutDocs ctx={ctx} project={project} co={co} editable={editable && !closed} />}
      {sub === 'lessons' && <CloseoutLessons ctx={ctx} project={project} co={co} editable={editable} />}
      {sub === 'summary' && <CloseoutSummary ctx={ctx} project={project} co={co} editable={editable && !closed} />}
    </div>
  );
}

function CloseoutChecklist({ ctx, project, co, editable }) {
  const [adding, setAdding] = useState(false);
  const [label, setLabel] = useState('');
  const [group, setGroup] = useState(CLOSEOUT_GROUPS[0]);
  return (
    <div className="space-y-3">
      {CLOSEOUT_GROUPS.map(g => {
        const items = co.checklist.filter(i => i.group === g);
        if (!items.length) return null;
        const open = items.filter(i => i.state === 'Open').length;
        return (
          <Collapsible key={g} id={`co-${project.id}-${g}`} defaultOpen title={g} count={items.length}
            right={<span className={`text-xs font-semibold ${open ? 'text-[var(--leon-brown)]' : 'text-green-700'}`}>
              {open ? `${open} open` : 'all settled'}</span>}>
            <div className="divide-y divide-[var(--leon-line)]">
              {items.map(it => (
                <div key={it.id} className="py-2 flex items-start gap-3 flex-wrap">
                  <select value={it.state} disabled={!editable}
                    onChange={e => ctx.setCloseoutItem(project.id, it.id, { state: e.target.value })}
                    className={`px-2 py-1 text-xs font-semibold border rounded w-20 ${it.state === 'Done' ? 'border-green-600 text-green-700 bg-green-50' : it.state === 'N/A' ? 'border-[var(--leon-line)] text-[var(--leon-black)]/45' : 'border-[var(--leon-brown)] text-[var(--leon-brown)]'}`}>
                    {CLOSEOUT_ITEM_STATES.map(x => <option key={x}>{x}</option>)}
                  </select>
                  <div className="flex-1 min-w-[220px]">
                    <div className={`text-sm ${it.state === 'N/A' ? 'line-through opacity-50' : ''}`}>{it.label}</div>
                    {(it.by || it.date) && (
                      <div className="text-[11px] text-[var(--leon-black)]/45">
                        {it.state} {it.date ? fmtDate(it.date) : ''}{it.by ? ` · ${it.by}` : ''}
                      </div>
                    )}
                  </div>
                  <TextInput className="w-56" placeholder="Note" disabled={!editable} value={it.note || ''}
                    onChange={e => ctx.setCloseoutItem(project.id, it.id, { note: e.target.value })} />
                  <FileField name={it.file} url={it.fileUrl} editable={editable} projectId={project.id}
                    label={it.label} placeholder="No evidence"
                    onChange={(file, fileUrl) => ctx.setCloseoutItem(project.id, it.id, { file, fileUrl })} />
                  {editable && it.custom && (
                    <IconBtn title="Remove this item" onClick={() => ctx.removeCloseoutItem(project.id, it.id)}>✕</IconBtn>
                  )}
                </div>
              ))}
            </div>
          </Collapsible>
        );
      })}
      {editable && (
        <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-3">
          {adding ? (
            <div className="flex items-end gap-2 flex-wrap">
              <Field label="Item" className="flex-1 min-w-[220px]">
                <TextInput value={label} onChange={e => setLabel(e.target.value)} placeholder="What else has to be true?" />
              </Field>
              <Field label="Group">
                <Select value={group} onChange={e => setGroup(e.target.value)}>
                  {CLOSEOUT_GROUPS.map(x => <option key={x}>{x}</option>)}
                </Select>
              </Field>
              <Button size="sm" disabled={!label.trim()} onClick={() => {
                ctx.addCloseoutItem(project.id, label.trim(), group); setLabel(''); setAdding(false);
              }}>Add</Button>
              <Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button>
            </div>
          ) : (
            <button onClick={() => setAdding(true)} className="text-sm font-semibold text-[var(--leon-brown)]">+ Add a checklist item</button>
          )}
          <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
            A standard item that does not apply to this job is marked <b>N/A</b> rather than removed, so the
            same list still means the same thing across every job. Only items added here can be deleted.
          </p>
        </div>
      )}
    </div>
  );
}

// The finished photographs. These are also what the Finished Projects library
// draws on, which is why publishing is decided here rather than centrally —
// the person who ran the job knows whether it is worth showing.
function CloseoutPhotos({ ctx, project, co, editable }) {
  const fileRef = useRef(null);
  const [busy, setBusy] = useState(false);
  const pf = co.portfolio || {};
  async function onPick(e) {
    const files = [...e.target.files];
    if (!files.length) return;
    setBusy(true);
    const read = await Promise.all(files.map(async f => ({ file: f.name, fileUrl: await readFileAsDataURL(f) })));
    ctx.addCloseoutPhotos(project.id, read);
    setBusy(false); e.target.value = '';
  }
  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
          The job as it was handed over. Caption them while it is fresh — in two years these are the
          only record of what was built, and an uncaptioned photo of a white kitchen could be any job.
        </p>
        {editable && (
          <>
            <Button onClick={() => fileRef.current.click()} disabled={busy}>{busy ? 'Adding…' : '+ Add photos'}</Button>
            <input ref={fileRef} type="file" accept="image/*" multiple className="hidden" onChange={onPick} />
          </>
        )}
      </div>

      {!co.photos.length && <EmptyState text="No finished photos yet." />}

      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
        {co.photos.map(ph => (
          <div key={ph.id} className={`rounded-lg border bg-white overflow-hidden ${pf.coverPhotoId === ph.id ? 'border-[var(--leon-brown)] ring-1 ring-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
            <div className="bg-[var(--leon-cream)]">
              <Photo src={ph.fileUrl} alt={ph.caption || ph.file} title={ph.file} caption={ph.caption} className="w-full h-44 object-cover" />
            </div>
            <div className="p-2.5 space-y-1.5">
              <TextInput placeholder="Caption — what is this?" disabled={!editable} value={ph.caption}
                onChange={e => ctx.updateCloseoutPhoto(project.id, ph.id, { caption: e.target.value })} />
              <div className="flex gap-1.5">
                <TextInput className="flex-1" placeholder="Room / area" disabled={!editable} value={ph.room}
                  onChange={e => ctx.updateCloseoutPhoto(project.id, ph.id, { room: e.target.value })} />
                <Select className="flex-1" disabled={!editable} value={ph.scopeId || ''}
                  onChange={e => ctx.updateCloseoutPhoto(project.id, ph.id, { scopeId: e.target.value || null })}>
                  <option value="">— scope —</option>
                  {(project.scopes || []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
                </Select>
              </div>
              <div className="flex items-center gap-2 text-[11px]">
                {editable && (
                  <button onClick={() => ctx.setCloseoutPortfolio(project.id, { coverPhotoId: pf.coverPhotoId === ph.id ? null : ph.id })}
                    className="font-semibold text-[var(--leon-brown)]">
                    {pf.coverPhotoId === ph.id ? '★ Cover' : '☆ Make cover'}
                  </button>
                )}
                <span className="ml-auto text-[var(--leon-black)]/35">{ph.addedBy}</span>
                {editable && <IconBtn title="Remove photo" onClick={() => ctx.removeCloseoutPhoto(project.id, ph.id)}>✕</IconBtn>}
              </div>
            </div>
          </div>
        ))}
      </div>

      {/* Publishing is a separate decision from finishing */}
      <Collapsible id={`co-portfolio-${project.id}`} title="Finished Projects library"
        right={<Badge tone={pf.published ? 'green' : 'neutral'}>{pf.published ? 'Published' : 'Not published'}</Badge>}>
        <div className="space-y-3">
          <p className="text-sm text-[var(--leon-black)]/60">
            Publishing puts this job in <b>LEON Library &rarr; Finished Projects</b>, where the whole team can
            use it as a portfolio piece. Closing a job and publishing it are separate decisions — a job can be
            finished and still not be one you would show.
          </p>
          <div className="grid gap-3 md:grid-cols-2">
            <Field label="Portfolio title" hint="Leave blank to use the project name.">
              <TextInput disabled={!editable} value={pf.title || ''}
                onChange={e => ctx.setCloseoutPortfolio(project.id, { title: e.target.value })} />
            </Field>
            <Field label="Tags" hint="Comma separated — how someone would search for it.">
              <TextInput disabled={!editable} value={(pf.tags || []).join(', ')}
                onChange={e => ctx.setCloseoutPortfolio(project.id, { tags: e.target.value.split(',').map(x => x.trim()).filter(Boolean) })} />
            </Field>
          </div>
          <Field label="Blurb" hint="Two or three lines — what the job was and what we did on it.">
            <TextArea rows={3} disabled={!editable} value={pf.blurb || ''}
              onChange={e => ctx.setCloseoutPortfolio(project.id, { blurb: e.target.value })} />
          </Field>
          <label className="flex items-center gap-2 text-sm">
            <input type="checkbox" disabled={!editable} checked={!!pf.hideClient}
              onChange={e => ctx.setCloseoutPortfolio(project.id, { hideClient: e.target.checked })} />
            <span>Hide the client's name in the library <span className="text-[var(--leon-black)]/45">— some clients do not want their job shown by name.</span></span>
          </label>
          {editable && (
            <div className="flex items-center gap-2">
              <Button variant={pf.published ? 'ghost' : undefined}
                disabled={!pf.published && !co.photos.length}
                title={!co.photos.length ? 'Add at least one photo first' : ''}
                onClick={() => ctx.setCloseoutPortfolio(project.id, { published: !pf.published })}>
                {pf.published ? 'Remove from the library' : 'Publish to the library'}
              </Button>
              {pf.published && <span className="text-xs text-[var(--leon-black)]/50">Published {fmtDate(pf.publishedDate)} by {pf.publishedBy}</span>}
            </div>
          )}
        </div>
      </Collapsible>
    </div>
  );
}

function CloseoutDocs({ ctx, project, co, editable }) {
  const [adding, setAdding] = useState(false);
  const [draft, setDraft] = useState({ type: CLOSEOUT_DOC_TYPES[0], name: '', file: null, fileUrl: null, note: '' });
  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
        What the client is owed at handover, in one place. These are the documents someone will come
        looking for two years from now when a warranty claim arrives.
      </p>
      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-sm min-w-[620px]">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-3 py-2">Type</th><th className="px-3 py-2">Name</th>
              <th className="px-3 py-2">Date</th><th className="px-3 py-2">File</th>
              <th className="px-3 py-2">Note</th><th className="px-3 py-2 w-8"></th>
            </tr>
          </thead>
          <tbody>
            {co.documents.map(d => (
              <tr key={d.id} className="border-b border-[var(--leon-line)]/60">
                <td className="px-3 py-1.5">
                  <Select className="w-52" disabled={!editable} value={d.type}
                    onChange={e => ctx.updateCloseoutDoc(project.id, d.id, { type: e.target.value })}>
                    {CLOSEOUT_DOC_TYPES.map(x => <option key={x}>{x}</option>)}
                  </Select>
                </td>
                <td className="px-3 py-1.5">
                  <TextInput className="w-48" disabled={!editable} value={d.name}
                    onChange={e => ctx.updateCloseoutDoc(project.id, d.id, { name: e.target.value })} />
                </td>
                <td className="px-3 py-1.5">
                  <TextInput type="date" className="w-36" disabled={!editable} value={d.date || ''}
                    onChange={e => ctx.updateCloseoutDoc(project.id, d.id, { date: e.target.value })} />
                </td>
                <td className="px-3 py-1.5">
                  <FileField name={d.file} url={d.fileUrl} editable={editable} projectId={project.id} label={d.type}
                    onChange={(file, fileUrl) => ctx.updateCloseoutDoc(project.id, d.id, { file, fileUrl })} />
                </td>
                <td className="px-3 py-1.5">
                  <TextInput className="w-44" disabled={!editable} value={d.note || ''}
                    onChange={e => ctx.updateCloseoutDoc(project.id, d.id, { note: e.target.value })} />
                </td>
                <td className="px-3 py-1.5">
                  {editable && <IconBtn title="Remove" onClick={() => ctx.removeCloseoutDoc(project.id, d.id)}>✕</IconBtn>}
                </td>
              </tr>
            ))}
            {!co.documents.length && <tr><td colSpan={6} className="px-3 py-4 text-center text-[var(--leon-black)]/40">Nothing filed yet.</td></tr>}
          </tbody>
        </table>
      </div>
      {editable && (adding ? (
        <div className="rounded-lg border border-[var(--leon-line)] p-3 flex items-end gap-2 flex-wrap">
          <Field label="Type"><Select value={draft.type} onChange={e => setDraft({ ...draft, type: e.target.value })}>
            {CLOSEOUT_DOC_TYPES.map(x => <option key={x}>{x}</option>)}</Select></Field>
          <Field label="Name" className="flex-1 min-w-[200px]">
            <TextInput value={draft.name} onChange={e => setDraft({ ...draft, name: e.target.value })} /></Field>
          <Field label="File"><FileField name={draft.file} url={draft.fileUrl} editable
            onChange={(file, fileUrl) => setDraft({ ...draft, file, fileUrl })} /></Field>
          <Button size="sm" onClick={() => { ctx.addCloseoutDoc(project.id, draft); setDraft({ type: CLOSEOUT_DOC_TYPES[0], name: '', file: null, fileUrl: null, note: '' }); setAdding(false); }}>Add</Button>
          <Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button>
        </div>
      ) : (
        <button onClick={() => setAdding(true)} className="text-sm font-semibold text-[var(--leon-brown)]">+ File a document</button>
      ))}
    </div>
  );
}

// What the job taught us. This is the one part of closeout that pays off on the
// NEXT job — and it is the honest source for tuning the lead-time libraries,
// which is why a lesson records the impact and the action, not just a grumble.
function CloseoutLessons({ ctx, project, co, editable }) {
  const [adding, setAdding] = useState(false);
  const blank = { category: 'Process', kind: 'Went badly', what: '', impact: '', action: '' };
  const [draft, setDraft] = useState(blank);
  const tone = k => k === 'Went well' ? 'green' : k === 'Would do differently' ? 'brown' : 'neutral';
  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
        The only part of closeout that pays off on the next job. Record what happened, what it cost in
        time or money, and what should change — a lesson with no action is just a complaint.
      </p>
      {co.lessons.map(l => (
        <div key={l.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-2">
          <div className="flex items-center gap-2 flex-wrap">
            <Select className="w-44" disabled={!editable} value={l.category}
              onChange={e => ctx.updateLesson(project.id, l.id, { category: e.target.value })}>
              {LESSON_CATEGORIES.map(x => <option key={x}>{x}</option>)}
            </Select>
            <Select className="w-52" disabled={!editable} value={l.kind}
              onChange={e => ctx.updateLesson(project.id, l.id, { kind: e.target.value })}>
              {LESSON_KINDS.map(x => <option key={x}>{x}</option>)}
            </Select>
            <Badge tone={tone(l.kind)}>{l.kind}</Badge>
            <span className="ml-auto text-[11px] text-[var(--leon-black)]/45">{l.loggedBy} · {fmtDate(l.date)}</span>
            {editable && <IconBtn title="Remove" onClick={() => ctx.removeLesson(project.id, l.id)}>✕</IconBtn>}
          </div>
          <div className="grid gap-2 md:grid-cols-3">
            <Field label="What happened"><TextArea rows={2} disabled={!editable} value={l.what}
              onChange={e => ctx.updateLesson(project.id, l.id, { what: e.target.value })} /></Field>
            <Field label="What it cost" hint="Days, dollars, or rework."><TextArea rows={2} disabled={!editable} value={l.impact}
              onChange={e => ctx.updateLesson(project.id, l.id, { impact: e.target.value })} /></Field>
            <Field label="What should change"><TextArea rows={2} disabled={!editable} value={l.action}
              onChange={e => ctx.updateLesson(project.id, l.id, { action: e.target.value })} /></Field>
          </div>
        </div>
      ))}
      {!co.lessons.length && <EmptyState text="Nothing logged yet." />}
      {editable && (adding ? (
        <div className="rounded-lg border border-[var(--leon-brown)] bg-white p-3 space-y-2">
          <div className="flex gap-2 flex-wrap">
            <Select className="w-44" value={draft.category} onChange={e => setDraft({ ...draft, category: e.target.value })}>
              {LESSON_CATEGORIES.map(x => <option key={x}>{x}</option>)}</Select>
            <Select className="w-52" value={draft.kind} onChange={e => setDraft({ ...draft, kind: e.target.value })}>
              {LESSON_KINDS.map(x => <option key={x}>{x}</option>)}</Select>
          </div>
          <div className="grid gap-2 md:grid-cols-3">
            <Field label="What happened"><TextArea rows={2} value={draft.what} onChange={e => setDraft({ ...draft, what: e.target.value })} /></Field>
            <Field label="What it cost"><TextArea rows={2} value={draft.impact} onChange={e => setDraft({ ...draft, impact: e.target.value })} /></Field>
            <Field label="What should change"><TextArea rows={2} value={draft.action} onChange={e => setDraft({ ...draft, action: e.target.value })} /></Field>
          </div>
          <div className="flex gap-2">
            <Button size="sm" disabled={!draft.what.trim()} onClick={() => { ctx.addLesson(project.id, draft); setDraft(blank); setAdding(false); }}>Log it</Button>
            <Button size="sm" variant="ghost" onClick={() => setAdding(false)}>Cancel</Button>
          </div>
        </div>
      ) : (
        <button onClick={() => setAdding(true)} className="text-sm font-semibold text-[var(--leon-brown)]">+ Log a lesson</button>
      ))}
    </div>
  );
}

// How the job actually finished against how it was sold. Read-only: every
// figure here already exists somewhere else, and a closeout that let you retype
// the contract value would just be a second, wrong copy of it.
function CloseoutSummary({ ctx, project, co, editable }) {
  const set = f => ctx.updateCloseout(project.id, f);
  const coTotal = (project.changeOrders || []).filter(c => c.status === 'Approved')
    .reduce((a, c) => a + (Number(c.amount) || 0), 0);
  const contract = Number(project.originalContractValue) || 0;
  const finalValue = contract + coTotal;
  const cost = Number(project.actualCostToDate) || 0;
  const margin = finalValue ? (finalValue - cost) / finalValue : 0;
  const openPunch = (project.punchItems || []).filter(p => p.status !== 'Closed' && p.status !== 'Complete').length;
  return (
    <div className="space-y-4">
      <div className="grid gap-3 md:grid-cols-4">
        {[
          ['Original contract', fmtMoney(contract)],
          ['Approved change orders', fmtMoney(coTotal)],
          ['Final contract value', fmtMoney(finalValue)],
          ['Cost to date', fmtMoney(cost)],
        ].map(([k, v]) => (
          <div key={k} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{k}</div>
            <div className="font-semibold">{v}</div>
          </div>
        ))}
      </div>
      <div className="rounded-lg border border-[var(--leon-brown)] bg-[var(--leon-brown)] text-white p-4 flex gap-6 flex-wrap items-center">
        <div>
          <div className="text-[10px] uppercase tracking-wide opacity-70">Final margin</div>
          <div className="text-2xl font-bold">{(Math.round(margin * 1000) / 10).toFixed(1)}%</div>
        </div>
        <div>
          <div className="text-[10px] uppercase tracking-wide opacity-70">Gross</div>
          <div className="text-lg font-semibold">{fmtMoney(finalValue - cost)}</div>
        </div>
        <div className="ml-auto text-[11px] opacity-80 text-right">
          Read from the job's own records — the Financial Hub is where these move.
          {openPunch ? <div className="mt-1 font-semibold">⚠ {openPunch} punch item{openPunch === 1 ? '' : 's'} still open</div> : null}
        </div>
      </div>

      <div className="grid gap-3 md:grid-cols-3">
        <Field label="Warranty starts" hint="Defaults to substantial completion.">
          <TextInput type="date" disabled={!editable} value={co.warrantyStartDate || ''}
            onChange={e => set({ warrantyStartDate: e.target.value || null })} />
        </Field>
        <Field label="Warranty period (months)">
          <TextInput type="number" disabled={!editable} value={co.warrantyMonths || ''}
            onChange={e => set({ warrantyMonths: e.target.value })} />
        </Field>
        <Field label="Warranty ends" hint="Calculated.">
          <div className="px-3 py-2 rounded border border-[var(--leon-line)] bg-[var(--leon-cream)] text-sm font-semibold">
            {warrantyEndDate(co) ? fmtDate(warrantyEndDate(co)) : '—'}
          </div>
        </Field>
      </div>
      <Field label="Warranty notes" hint="What is covered, what is not, who to call.">
        <TextArea rows={2} disabled={!editable} value={co.warrantyNotes || ''}
          onChange={e => set({ warrantyNotes: e.target.value })} />
      </Field>

      <Collapsible id={`co-signoff-${project.id}`} defaultOpen title="Client sign-off">
        <div className="grid gap-3 md:grid-cols-3">
          <Field label="Signed by"><TextInput disabled={!editable} value={co.clientSignOffName || ''}
            onChange={e => set({ clientSignOffName: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" disabled={!editable} value={co.clientSignOffDate || ''}
            onChange={e => set({ clientSignOffDate: e.target.value || null })} /></Field>
          <Field label="Signed document">
            <FileField name={co.clientSignOffFile} url={co.clientSignOffUrl} editable={editable}
              projectId={project.id} label="Client sign-off"
              onChange={(file, fileUrl) => set({ clientSignOffFile: file, clientSignOffUrl: fileUrl })} />
          </Field>
        </div>
      </Collapsible>

      <Field label="Closeout summary" hint="The paragraph someone reads in two years to remember this job.">
        <TextArea rows={4} disabled={!editable} value={co.summary || ''} onChange={e => set({ summary: e.target.value })} />
      </Field>
    </div>
  );
}

// ══════════════════════════════════════ Finished Projects library
// The portfolio. Reads across every project whose closeout was published —
// nothing is stored twice, so a photo recaptioned on the job updates here.
function FinishedProjectsLibrary({ ctx }) {
  const [q, setQ] = useState('');
  const [open, setOpen] = useState(null);
  const entries = (ctx.projects || [])
    .filter(p => p.closeout && p.closeout.portfolio && p.closeout.portfolio.published)
    .map(p => {
      const pf = p.closeout.portfolio;
      const cover = p.closeout.photos.find(x => x.id === pf.coverPhotoId) || p.closeout.photos[0];
      const acct = (ctx.accounts || []).find(a => a.id === p.accountId);
      return { p, pf, cover, client: pf.hideClient ? null : (acct && acct.name) || null };
    })
    .filter(e => {
      if (!q.trim()) return true;
      const hay = `${e.pf.title} ${e.p.name} ${e.client || ''} ${(e.pf.tags || []).join(' ')} ${e.pf.blurb}`.toLowerCase();
      return hay.includes(q.trim().toLowerCase());
    });

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h3 className="text-lg font-bold">Finished Projects</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            Work we have completed and chosen to show. Published from each job's Closeout tab by whoever
            ran it — so the photos, captions and scopes here are the job's own record, not a second copy.
          </p>
        </div>
        <Field label="Search"><TextInput className="w-52" value={q} onChange={e => setQ(e.target.value)}
          placeholder="Project, client, tag…" /></Field>
      </div>

      {!entries.length && (
        <EmptyState text={q ? 'Nothing matches that search.' : 'No finished projects published yet. Publish one from a job’s Closeout tab.'} />
      )}

      <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {entries.map(({ p, pf, cover, client }) => (
          <button key={p.id} onClick={() => setOpen(p.id)}
            className="text-left rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden hover:border-[var(--leon-brown)] transition">
            <div className="bg-[var(--leon-cream)] h-44">
              {cover
                ? <Photo src={cover.fileUrl} alt={pf.title || p.name} title={pf.title || p.name} caption={cover.caption} className="w-full h-44 object-cover" />
                : <div className="h-44 grid place-items-center text-3xl opacity-30">🏗️</div>}
            </div>
            <div className="p-3">
              <div className="font-bold leading-tight">{pf.title || p.name}</div>
              {client && <div className="text-xs text-[var(--leon-black)]/55">{client}</div>}
              {pf.blurb && <p className="text-xs text-[var(--leon-black)]/60 mt-1.5 line-clamp-3">{pf.blurb}</p>}
              <div className="flex flex-wrap gap-1 mt-2">
                {(pf.tags || []).slice(0, 4).map(t => <Badge key={t}>{t}</Badge>)}
              </div>
              <div className="text-[11px] text-[var(--leon-black)]/40 mt-2">
                {p.closeout.photos.length} photo{p.closeout.photos.length === 1 ? '' : 's'}
                {p.closeout.closedDate ? ` · completed ${fmtDate(p.closeout.closedDate)}` : ''}
              </div>
            </div>
          </button>
        ))}
      </div>

      <FinishedProjectModal ctx={ctx} projectId={open} onClose={() => setOpen(null)} />
    </div>
  );
}

function FinishedProjectModal({ ctx, projectId, onClose }) {
  const p = (ctx.projects || []).find(x => x.id === projectId);
  if (!p) return null;
  const pf = p.closeout.portfolio;
  const acct = (ctx.accounts || []).find(a => a.id === p.accountId);
  const client = pf.hideClient ? null : (acct && acct.name);
  return (
    <Modal open={!!p} onClose={onClose} wide title={pf.title || p.name}
      footer={
        <div className="flex items-center justify-between w-full gap-3">
          <div className="text-xs text-[var(--leon-black)]/50">
            {client ? `${client} · ` : ''}{p.closeout.photos.length} photos
            {p.closeout.closedDate ? ` · completed ${fmtDate(p.closeout.closedDate)}` : ''}
          </div>
          <ShareButton ctx={ctx} projectId={p.id} subjectKey={`portfolio:${p.id}`}
            subject={`${pf.title || p.name} — finished project`}
            summary={pf.blurb || 'Completed project'} label="Share" />
        </div>
      }>
      <div className="space-y-3">
        {pf.blurb && <p className="text-sm text-[var(--leon-black)]/70">{pf.blurb}</p>}
        <div className="flex flex-wrap gap-1">{(pf.tags || []).map(t => <Badge key={t}>{t}</Badge>)}</div>
        <div className="grid gap-3 sm:grid-cols-2">
          {p.closeout.photos.map(ph => {
            const sc = (p.scopes || []).find(s => s.id === ph.scopeId);
            return (
              <figure key={ph.id} className="rounded-lg overflow-hidden border border-[var(--leon-line)]">
                <Photo src={ph.fileUrl} alt={ph.caption || ph.file} title={ph.file} caption={ph.caption} className="w-full h-auto" />
                {(ph.caption || ph.room || sc) && (
                  <figcaption className="p-2 text-xs">
                    {ph.caption && <div className="font-medium">{ph.caption}</div>}
                    <div className="text-[var(--leon-black)]/50">
                      {[ph.room, sc && sc.name].filter(Boolean).join(' · ')}
                    </div>
                  </figcaption>
                )}
              </figure>
            );
          })}
        </div>
      </div>
    </Modal>
  );
}

// ══════════════════════════════════════════ Quote Analysis (Interiors)
// A pricing draft built off a take-off. It is deliberately a closed loop: it
// reads take-offs and scopes, and writes nothing but itself. No scope is
// created, no stage schedule is instantiated, no contract value moves. This is
// a bid — the job does not exist yet, and a quote that quietly built a
// production schedule would be very hard to unpick when the client says no.
// Turning an accepted analysis into contracted scopes is a separate, later
// step, taken on purpose.

function dataUrlToArrayBuffer(url) {
  const base64 = String(url || '').split(',')[1] || '';
  const bin = atob(base64);
  const buf = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  return buf.buffer;
}

// A number field that keeps "empty" distinct from "zero". Empty means inherit;
// zero means this line genuinely carries none — collapsing the two is how a
// freight-exempt line silently picks up 8%.
// `disabled` matters more here than it looks: every caller already guards its
// own write with `editable &&`, so a locked quotation was SAFE — but the field
// still took the keystroke and then threw it away, which is the worst of both.
// A control that cannot act must not look like it can.
function QNum({ value, onChange, placeholder, prefix, suffix, w, align, disabled }) {
  const [draft, setDraft] = useState(null);
  const shown = draft !== null ? draft : (value === null || value === undefined ? '' : String(value));
  return (
    <span className="inline-flex items-center gap-0.5">
      {prefix && <span className="text-[10px] text-[var(--leon-black)]/40">{prefix}</span>}
      <input
        type="text" inputMode="decimal" value={shown} placeholder={placeholder || ''} disabled={disabled}
        onChange={e => setDraft(e.target.value)}
        onBlur={e => {
          setDraft(null);
          const t = e.target.value.trim();
          if (t === '') { onChange(null); return; }
          const n = parseFloat(t.replace(/[^0-9.\-]/g, ''));
          onChange(isFinite(n) ? n : null);
        }}
        className={`${w || 'w-20'} px-1.5 py-1 text-xs border border-[var(--leon-line)] rounded ${disabled ? 'bg-[var(--leon-cream)]/60 text-[var(--leon-black)]/45' : 'bg-white'} ${align || 'text-right'} focus:outline-none focus:border-[var(--leon-brown)]`}
      />
      {suffix && <span className="text-[10px] text-[var(--leon-black)]/40">{suffix}</span>}
    </span>
  );
}
// Percentages are stored as fractions and typed as whole numbers, because
// nobody writes 0.35 on a quote.
function QPct({ value, onChange, placeholder, w, disabled }) {
  return <QNum w={w || 'w-16'} suffix="%" placeholder={placeholder} disabled={disabled}
    value={value === null || value === undefined ? null : Math.round(value * 10000) / 100}
    onChange={n => onChange(n === null ? null : n / 100)} />;
}

function pct(n) { return `${(Math.round((n || 0) * 1000) / 10).toFixed(1)}%`; }
function qty4(n) { return (Math.round((n || 0) * 100) / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function marginTone(p) { return p >= 0.30 ? 'green' : p >= 0.18 ? 'amber' : 'red'; }

// Freight and duty each pick a BASIS and then the one figure that basis needs.
// The same control serves a scope and a single line: at scope level it sets the
// default, at line level it overrides it — which is why "Follow the scope" is
// an option only when there is a scope above to follow.
function QuoteChargeEditor({ which, obj, parent, qa, onChange, editable, canInherit, showCbm, inheritLabel }) {
  const BASES = which === 'freight' ? QUOTE_FREIGHT_BASES : QUOTE_DUTY_BASES;
  const own = obj[`${which}Basis`];
  const effective = own || (parent && parent[`${which}Basis`]) || qa[`${which}Basis`] || 'pct';
  const inherited = f => {
    const v = parent && parent[f] !== null && parent[f] !== undefined && parent[f] !== '' ? parent[f] : qa[f];
    return v === null || v === undefined || v === '' ? '' : String(v);
  };
  const label = which === 'freight' ? 'Freight' : 'Duty / tariff';
  return (
    <div className="rounded border border-[var(--leon-line)] bg-white p-2.5 space-y-2">
      <div className="flex items-center gap-2">
        <span className="text-[10px] font-semibold uppercase tracking-wide text-[var(--leon-black)]/55">{label}</span>
        <select value={own || ''} disabled={!editable} onChange={e => onChange({ [`${which}Basis`]: e.target.value || null })}
          className="ml-auto px-1.5 py-0.5 text-[11px] border border-[var(--leon-line)] rounded bg-white">
          {canInherit && <option value="">{inheritLabel || 'Follow the scope'}</option>}
          {BASES.map(b => <option key={b.key} value={b.key}>{b.label}</option>)}
        </select>
      </div>
      <div className="flex items-center gap-2 flex-wrap text-[11px]">
        {effective === 'pct' && (
          <label className="flex items-center gap-1">of material
            <QPct w="w-16" placeholder={inherited(`${which}Pct`) ? String(Math.round(Number(inherited(`${which}Pct`)) * 100)) : ''}
              value={obj[`${which}Pct`]} disabled={!editable} onChange={v => editable && onChange({ [`${which}Pct`]: v })} /></label>
        )}
        {effective === 'perUnit' && (
          <label className="flex items-center gap-1">per unit
            <QNum w="w-20" prefix="$" placeholder={inherited(`${which}PerUnit`)}
              value={obj[`${which}PerUnit`]} disabled={!editable} onChange={v => editable && onChange({ [`${which}PerUnit`]: v })} /></label>
        )}
        {effective === 'lump' && (
          <label className="flex items-center gap-1">lump sum
            <QNum w="w-24" prefix="$" value={obj[`${which}Lump`]} disabled={!editable} onChange={v => editable && onChange({ [`${which}Lump`]: v })} /></label>
        )}
        {effective === 'volume' && (
          <>
            {showCbm && (
              <label className="flex items-center gap-1">CBM / unit
                <QNum w="w-20" value={obj.cbmPerUnit} disabled={!editable} onChange={v => editable && onChange({ cbmPerUnit: v })} /></label>
            )}
            <label className="flex items-center gap-1">$ / CBM
              <QNum w="w-20" prefix="$" placeholder={inherited('freightPerCbm')}
                value={obj.freightPerCbm} disabled={!editable} onChange={v => editable && onChange({ freightPerCbm: v })} /></label>
            {!showCbm && <span className="text-[var(--leon-black)]/45">CBM per unit is set on each line.</span>}
          </>
        )}
        {which === 'duty' && effective === 'pct' && (
          <label className="flex items-center gap-1 ml-auto">HTS
            <input value={obj.htsCode === undefined ? '' : obj.htsCode} disabled={!editable || obj.htsCode === undefined}
              onChange={e => onChange({ htsCode: e.target.value })} placeholder="9403.60"
              className="w-24 px-1.5 py-1 text-[11px] border border-[var(--leon-line)] rounded bg-white" /></label>
        )}
      </div>
    </div>
  );
}

function QuoteDraftSubTab({ ctx, project }) {
  // A ?quote= link opens this draft straight away, then clears itself so a
  // later Back does not keep re-opening it.
  const [openId, setOpenId] = useState(() => ctx.bootQuoteId || null);
  // Which revision the wizard should reopen on. Set when one is forked, so
  // "new revision" lands back in the guided flow rather than on a full screen
  // of fields — which is what the client asked for.
  const [wizardFor, setWizardFor] = useState(null);
  useEffect(() => { if (ctx.bootQuoteId) ctx.clearBootQuote(); }, []);
  const [generating, setGenerating] = useState(false);
  const editable = ctx.canEdit('quotes');
  const list = project.quoteAnalyses || [];
  const open = list.find(q => q.id === openId);

  // Interiors only, as asked. Windows are priced off the Window Schedule's own
  // lead-time and system data, which is a different shape entirely — forcing
  // both through one screen would serve neither.
  if (ctx.activeDepartment === 'Windows') {
    return (
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-6 text-center">
        <div className="text-3xl mb-2">🧮</div>
        <div className="font-semibold mb-1">Quote Analysis is for Interiors</div>
        <div className="text-sm text-[var(--leon-black)]/60 max-w-lg mx-auto">
          Window and exterior door systems are priced off the Window Schedule's system, finish and
          glass data, which is a different shape from an interiors take-off. Switch the department
          selector to Interiors to work on a quote here.
        </div>
      </div>
    );
  }

  if (open) return <QuoteAnalysisDetail ctx={ctx} project={project} qa={open}
    onBack={() => setOpenId(null)} onOpen={setOpenId} editable={editable}
    startInWizard={wizardFor === open.id} onWizardDone={() => setWizardFor(null)}
    onRevised={id => { setWizardFor(id); setOpenId(id); }} />;

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="text-lg font-bold">Quote Analysis</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            Price a bid off a take-off, scope by scope. Nothing here creates a scope, a schedule or a
            contract value — it is a working draft until you decide otherwise.
          </p>
        </div>
        {/* Only when there is already a list. With none, the empty state below
            carries this same button along with the other ways to start, and two
            identical buttons on one screen is a question about which to press. */}
        {editable && !!list.length && <Button onClick={() => setGenerating(true)}>✨ Generate Quote Analysis</Button>}
      </div>

      {!list.length && (
        <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-8 text-center">
          <div className="text-3xl mb-2">🧮</div>
          <div className="font-semibold mb-1">No quote analysis yet</div>
          <div className="text-sm text-[var(--leon-black)]/60 mb-4 max-w-md mx-auto">
            Generate one from a take-off already filed on this job, from a LEON Take-Off Template
            workbook, or start blank and build it scope by scope.
          </div>
          {editable && (
            <div className="flex items-center justify-center gap-2 flex-wrap">
              <Button onClick={() => setGenerating(true)}>✨ Generate Quote Analysis</Button>
              <Button variant="ghost" onClick={() => setOpenId(ctx.loadDemoQuoteAnalysis(project.id))}>
                See a worked example
              </Button>
            </div>
          )}
        </div>
      )}

      <div className="grid gap-3 md:grid-cols-2">
        {list.map(qa => {
          const t = quoteAnalysisTotals(qa);
          return (
            <button key={qa.id} onClick={() => setOpenId(qa.id)}
              className="text-left rounded-lg border border-[var(--leon-line)] bg-white p-4 hover:border-[var(--leon-brown)] transition">
              <div className="flex items-start justify-between gap-2 mb-2">
                <div>
                  <div className="font-bold">{qa.name}</div>
                  <div className="text-xs text-[var(--leon-black)]/50">
                    Rev {qa.revision} · {qa.preparedBy || 'Unassigned'} · {fmtDate(qa.createdDate)}
                  </div>
                </div>
                <Badge tone={qa.status === 'Issued' ? 'green' : qa.status === 'Superseded' ? 'neutral' : 'brown'}>{qa.status}</Badge>
              </div>
              <div className="grid grid-cols-3 gap-2 text-center pt-2 border-t border-[var(--leon-line)]">
                <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Cost</div><div className="font-semibold text-sm">{fmtMoney(t.cost)}</div></div>
                <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Sell</div><div className="font-bold text-sm text-[var(--leon-brown)]">{fmtMoney(t.sell)}</div></div>
                <div><div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">Margin</div><div className={`font-semibold text-sm ${t.marginPct >= 0.18 ? '' : 'text-red-600'}`}>{pct(t.marginPct)}</div></div>
              </div>
              <div className="text-[11px] text-[var(--leon-black)]/45 mt-2">
                {t.sections} scope{t.sections === 1 ? '' : 's'} · {t.lines} line{t.lines === 1 ? '' : 's'}
                {t.unpriced ? ` · ${t.unpriced} unpriced` : ''}
                {qa.warnings && qa.warnings.length ? ` · ${qa.warnings.length} import note${qa.warnings.length === 1 ? '' : 's'}` : ''}
              </div>
            </button>
          );
        })}
      </div>

      <GenerateQuoteAnalysisModal open={generating} onClose={() => setGenerating(false)} ctx={ctx} project={project}
        onCreated={(id, newWindow) => {
          setGenerating(false);
          // Pricing a bid is long work you want beside the take-off it came
          // from, so a new draft can open in its own window. The link carries
          // the job and the draft; ?quote= boots straight into it.
          if (newWindow) openQuoteInNewTab(project.id, id);
          else setOpenId(id);
        }} />
    </div>
  );
}

// Two ways in, one parser behind them. "From a take-off" reads the workbook
// already attached to a take-off record on this job; "Upload" reads one you
// pick. Both land in the same review step, so an import is never committed
// sight-unseen.
// One scope, on its own. Everything the wizard asks here is a decision about
// THIS scope — how it is priced and what its cost recipe is — with its lines
// shown so the quantities can be checked against the take-off they came from.
// Nothing is written to the project: the wizard edits its own copy and the
// draft is created once, at the end.
// Converting a quotation into contracted scopes. Deliberately a review screen
// and not a button that just does it: this is the moment a bid becomes work,
// it creates a stage schedule per scope, and it cannot be undone by pressing
// the button again.
function ConvertQuoteModal({ open, onClose, ctx, project, qa, onDone }) {
  const [startDate, setStartDate] = useState(todayISO());
  const [only, setOnly] = useState({});
  useEffect(() => {
    if (!open) return;
    setStartDate(todayISO());
    const all = {};
    (qa.sections || []).forEach(sec => { all[sec.id] = true; });
    setOnly(all);
  }, [open, qa.id]);
  if (!qa) return null;
  const chosen = (qa.sections || []).filter(sec => only[sec.id]);
  const total = chosen.reduce((a, sec) => a + quoteSectionTotals(sec, qa).sell, 0);

  function go() {
    const made = ctx.convertQuoteToScopes(project.id, qa.id, {
      startDate, only: chosen.map(sec => sec.id),
    });
    onClose();
    if (made && made.length && onDone) onDone();
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Convert this quotation to contracted scopes"
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <div className="flex-1" />
               <Button disabled={!chosen.length} onClick={go}>
                 Create {chosen.length} scope{chosen.length === 1 ? '' : 's'}
               </Button></>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/70">
          Each scope below becomes a real scope on this job, with its own stage schedule built from its
          family&rsquo;s lead times. <strong>The quotation itself is not changed</strong> &mdash; it stays the
          record of what was agreed.
        </p>

        <Field label="Work starts" hint="Every schedule is calculated from this date.">
          <TextInput type="date" className="!w-44" value={startDate} onChange={e => setStartDate(e.target.value)} />
        </Field>

        <div className="border border-[var(--leon-line)] rounded-lg overflow-hidden">
          <table className="w-full text-sm">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
                <th className="px-3 py-2 w-8"></th><th className="px-3 py-2">Scope</th>
                <th className="px-3 py-2">Becomes</th><th className="px-3 py-2 text-right">Contract value</th>
              </tr>
            </thead>
            <tbody>
              {(qa.sections || []).map(sec => {
                const t = quoteSectionTotals(sec, qa);
                const type = sec.kind === 'labor' ? 'Labor Only'
                  : sec.kind === 'combined' ? COMBINED_SCOPE_TYPE : 'Supply Only';
                return (
                  <tr key={sec.id} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-1.5">
                      <input type="checkbox" checked={!!only[sec.id]}
                        onChange={e => setOnly(o => ({ ...o, [sec.id]: e.target.checked }))} />
                    </td>
                    <td className="px-3 py-1.5 font-semibold">{sec.name}</td>
                    <td className="px-3 py-1.5 text-[var(--leon-black)]/60">
                      {type}
                      {!t.sell && <span className="ml-2 text-[var(--leon-red)] text-xs">no price</span>}
                    </td>
                    <td className="px-3 py-1.5 text-right tabular-nums">{fmtMoney(t.sell)}</td>
                  </tr>
                );
              })}
            </tbody>
            <tfoot>
              <tr className="border-t-2 border-[var(--leon-black)] font-bold bg-[var(--leon-cream)]">
                <td /><td className="px-3 py-2">{chosen.length} selected</td><td />
                <td className="px-3 py-2 text-right tabular-nums">{fmtMoney(total)}</td>
              </tr>
            </tfoot>
          </table>
        </div>

        <p className="text-[11px] text-[var(--leon-black)]/55">
          The quoted sell becomes each scope&rsquo;s contract value and the quoted cost its budget, so the job
          starts out measured against what was actually sold. <strong>This runs once</strong> &mdash; the
          quotation records that it was converted, so a second press cannot double the job.
        </p>
      </div>
    </Modal>
  );
}
// What a built wizard section IS, independent of where it sits in the list and
// of the id it was given this rebuild: the scope it came from plus how it is
// being sold. Both halves matter — Casework Supply and Casework Labor are two
// different things to price.
function quoteWizardKey(sec) {
  if (!sec) return '';
  return `${sec.scopeKey || sec.name || ''}|${sec.kind || ''}`;
}

// A band of a scope: a tinted, clickable HEADER and a plain body. The colour
// belongs on the title bar rather than behind the fields — a whole tinted panel
// fights the inputs sitting on it, and the header alone is what tells you which
// of the three you are in. Folds, and remembers.
function QuoteBand({ id, n, title, right, tone, defaultOpen, children }) {
  const TONES = {
    sales: 'bg-[var(--leon-brown)]/25 border-[var(--leon-brown)]/35',
    costs: 'bg-[var(--leon-yellow)]/25 border-[var(--leon-yellow)]/40',
    lines: 'bg-[var(--leon-cream)] border-[var(--leon-line)]',
  };
  const [open, setOpen] = useState(defaultOpen !== false);
  return (
    <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden bg-white">
      <button type="button" onClick={() => setOpen(o => !o)}
        className={`w-full flex items-center gap-2 px-3 py-2 border-b text-left ${TONES[tone] || TONES.lines}`}>
        <span className="text-[10px] text-[var(--leon-black)]/40 w-3">{open ? '\u25BE' : '\u25B8'}</span>
        <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/70">
          {n} &middot; {title}
        </span>
        <span className="flex-1" />
        {right && <span className="text-[11px] text-[var(--leon-black)]/55 font-normal">{right}</span>}
      </button>
      {open && <div className="p-3">{children}</div>}
    </div>
  );
}

// A classification's live duty rate, as a FRACTION. The library stores a percent
// number (25.0) on the current VERSION, not on the classification; a quotation
// stores fractions. Converting in one place is what stops a 25% duty being
// quoted as 2500%.
function tariffRatePct(c) {
  if (!c) return 0;
  const cur = currentTariffVersion(c) || {};
  return (qnum(cur.totalEstimatedDutyPct) || 0) / 100;
}

// 2 · LOGISTICS COSTS — what it costs to get the goods here and cleared.
// Named for what it is rather than "additional costs", which said nothing.
// The container legs sit directly under the freight control that selects them,
// because "by the container" is a question with four follow-ups and reading
// them three panels away is how one gets left blank. Each half states its own
// TOTAL, since a basis and a rate are not an answer — the money is.
function QuoteScopeLogistics({ sec, qa, set, editable, ctx }) {
  const st = quoteSectionTotals(sec, qa);
  const byContainer = quoteFreightBasisFor(null, sec, qa) === 'container';
  const cls = (ctx && ctx.tariffLibrary) || [];
  const chosen = cls.find(c => c.id === sec.tariffClassificationId) || null;
  const [q, setQ] = useState('');
  const matches = useMemo(() => {
    const s2 = q.trim().toLowerCase();
    if (!s2) return [];
    return cls.filter(c => `${c.htsCode || ''} ${c.materialCategory || ''} ${c.countryOfOrigin || ''} ${c.productDescription || ''}`
      .toLowerCase().includes(s2)).slice(0, 25);
  }, [q, cls]);
  return (
    <QuoteBand id="wiz-logistics" n="2" tone="costs" title="Logistics costs"
      right={`${fmtMoney(st.freight + st.duty)} freight + duty`}>
      <div className="grid md:grid-cols-2 gap-4">
        {/* FREIGHT, with the container questions inline under it. */}
        <div>
          <QuoteChargeEditor which="freight" obj={sec} parent={qa} qa={qa}
            onChange={set} editable={editable} canInherit inheritLabel="Follow the job" showCbm={false} />
          {byContainer && (
            <div className="mt-2 rounded border border-[var(--leon-line)] bg-white p-2.5">
              <div className="grid grid-cols-2 gap-2">
                <Field label="Fit one container" hint="How many of what the scope counts.">
                  <QNum w="w-24" value={sec.containerCapacity} disabled={!editable}
                    onChange={v => editable && set({ containerCapacity: v })} />
                </Field>
                <Field label="Ocean freight" hint="Per container.">
                  <QNum w="w-24" prefix="$" value={sec.freightPerContainer} disabled={!editable}
                    onChange={v => editable && set({ freightPerContainer: v })} />
                </Field>
                <Field label="Inland" hint="Per container.">
                  <QNum w="w-24" prefix="$" value={sec.inlandPerContainer} disabled={!editable}
                    onChange={v => editable && set({ inlandPerContainer: v })} />
                </Field>
                <Field label="Broker" hint="Per container.">
                  <QNum w="w-24" prefix="$" value={sec.brokerPerContainer} disabled={!editable}
                    onChange={v => editable && set({ brokerPerContainer: v })} />
                </Field>
              </div>
              <QuoteScopeContainerChain st={st} sec={sec} />
            </div>
          )}
          <div className="mt-2 flex items-baseline gap-2 text-[13px] border-t border-[var(--leon-line)] pt-2">
            <span className="font-semibold">Total for freight</span>
            <span className="flex-1" />
            <span className="font-bold tabular-nums">{fmtMoney(st.freight)}</span>
          </div>
        </div>

        {/* DUTY, answered from the tariff library rather than typed. */}
        <div>
          <QuoteChargeEditor which="duty" obj={sec} parent={qa} qa={qa}
            onChange={set} editable={editable} canInherit inheritLabel="Follow the job" showCbm={false} />
          <div className="mt-2 rounded border border-[var(--leon-line)] bg-white p-2.5">
            <div className="text-[10px] font-semibold uppercase tracking-wide text-[var(--leon-black)]/55 mb-1">
              From the tariff library
            </div>
            {chosen ? (
              <div className="flex items-start gap-2">
                <div className="min-w-0 flex-1">
                  <div className="text-xs font-semibold">{chosen.htsCode} &middot; {chosen.materialCategory}</div>
                  <div className="text-[11px] text-[var(--leon-black)]/55 truncate">
                    from {chosen.countryOfOrigin} &middot; {pct(tariffRatePct(chosen))} estimated duty
                  </div>
                </div>
                {editable && (
                  <button type="button" className="text-[11px] text-[var(--leon-black)]/40 hover:text-[var(--leon-red)]"
                    onClick={() => set({ tariffClassificationId: '' })}>Clear</button>
                )}
              </div>
            ) : (
              <>
                <TextInput className="!w-full" placeholder="Search an HTS code, product or country"
                  value={q} disabled={!editable} onChange={e => setQ(e.target.value)} />
                {!!matches.length && (
                  <div className="mt-1 max-h-40 overflow-y-auto border border-[var(--leon-line)] rounded">
                    {matches.map(c => (
                      <button key={c.id} type="button"
                        onClick={() => { set({ tariffClassificationId: c.id, dutyBasis: 'pct', dutyPct: tariffRatePct(c) }); setQ(''); }}
                        className="w-full text-left px-2 py-1 text-[11px] hover:bg-[var(--leon-cream)] border-b border-[var(--leon-line)] last:border-0">
                        <span className="font-semibold">{c.htsCode}</span> &middot; {c.materialCategory}
                        <span className="text-[var(--leon-black)]/45"> &middot; {c.countryOfOrigin} &middot; {pct(tariffRatePct(c))}</span>
                      </button>
                    ))}
                  </div>
                )}
                <p className="text-[10px] text-[var(--leon-black)]/45 mt-1">
                  Picking one sets this scope&rsquo;s duty rate from the library. A line may still carry its own.
                </p>
              </>
            )}
          </div>
          <div className="mt-2 flex items-baseline gap-2 text-[13px] border-t border-[var(--leon-line)] pt-2">
            <span className="font-semibold">Total for tariff</span>
            <span className="flex-1" />
            <span className="font-bold tabular-nums">{fmtMoney(st.duty)}</span>
          </div>
        </div>
      </div>
    </QuoteBand>
  );
}

// The scope's specification for ONE line, in the wizard. Folded by default —
// nine fields under every row would bury the quantities — and it states the
// client's own chain underneath, because on a casework line the two figures
// people read are the type rate and the vendor cost, and both are derived.
// The specification for a whole AREA — asked once, and inherited by every item
// beneath the heading. This is where a casework quotation is actually written:
// "Level 4, Units 401-412" and then one answer each for the box, the doors, the
// handle and the LED, rather than the same nine answers on every line.
function QuoteAreaSpec({ ctx, sec, area, set, editable }) {
  const canEdit = editable !== false;
  const [open, setOpen] = useState(false);
  const fields = quoteSpecFieldsFor(sec.scopeKey || sec.name);
  if (!fields.length) return null;
  const specs = area.specs || {};
  const answered = fields.filter(f => specs[f]).length;
  const covers = (() => {
    const rows = sec.lines || [];
    const i = rows.indexOf(area);
    let n = 0;
    for (let k = i + 1; k < rows.length; k++) {
      if (rows[k].rowKind === 'area') break;
      if (quoteRowIsItem(rows[k])) n++;
    }
    return n;
  })();
  return (
    <div className="rounded border border-[var(--leon-brown)]/30 bg-white">
      <button type="button" onClick={() => setOpen(o => !o)}
        className="w-full flex items-center gap-2 px-2 py-1 text-left">
        <span className="text-[10px] text-[var(--leon-black)]/40">{open ? '\u25BE' : '\u25B8'}</span>
        <span className="text-[10px] uppercase tracking-wide font-semibold text-[var(--leon-brown)]">
          Specification for this area
        </span>
        <span className="text-[10px] text-[var(--leon-black)]/45">
          {answered} of {fields.length} answered &middot; applies to {covers} line{covers === 1 ? '' : 's'} below
        </span>
      </button>
      {open && (
        <div className="px-2 pb-2 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
          {fields.map(f => (
            <QuoteSpecField key={f} ctx={ctx} name={f} line={area} set={set} editable={canEdit} />
          ))}
        </div>
      )}
    </div>
  );
}

function QuoteWizardLineSpec({ ctx, sec, line, set }) {
  const [open, setOpen] = useState(false);
  // The SPECIFICATION is answered once on the area, not here. A line carries
  // only what genuinely differs line by line: how many modules, how many of the
  // type, and the rate. Repeating nine specification fields under every line
  // was the same answer typed over and over, which is exactly what moving it to
  // the area was meant to stop.
  const isModule = (sec.costDriver || 'qty') === 'module';
  if (!isModule) return null;
  // Fall back to the line's own quantity so a take-off line reads its real
  // figures rather than $0 — quoteLineQty is the engine's own "how many".
  const modules = qnum(line.qtyPerItem) || quoteLineQty(line);
  const types = qnum(line.itemCount) || 1;
  const typeRate = modules * qnum(line.matUnit);
  const vendorCost = typeRate * types;
  return (
    <div className="rounded border border-[var(--leon-line)] bg-white">
      <button type="button" onClick={() => setOpen(o => !o)}
        className="w-full flex items-center gap-2 px-2 py-1 text-left">
        <span className="text-[10px] text-[var(--leon-black)]/40">{open ? '\u25BE' : '\u25B8'}</span>
        <span className="text-[10px] uppercase tracking-wide font-semibold text-[var(--leon-black)]/50">
          Quantities
        </span>
        <span className="flex-1" />
        <span className="text-[10px] tabular-nums text-[var(--leon-black)]/55">
          vendor cost {fmtMoney(vendorCost)}
        </span>
      </button>
      {open && (
        <div className="px-2 pb-2">
          <div className="grid gap-2 sm:grid-cols-3">
            <Field label="Module count" hint={qnum(line.qtyPerItem) ? null : 'Blank — using the line quantity.'}>
              <QNum w="w-24" placeholder={String(quoteLineQty(line) || '')} value={line.qtyPerItem}
                onChange={v => set({ qtyPerItem: v })} />
            </Field>
            <Field label="Type quantity" hint={qnum(line.itemCount) ? null : 'Blank — counted as one.'}>
              <QNum w="w-24" placeholder="1" value={line.itemCount}
                onChange={v => set({ itemCount: v })} />
            </Field>
            <Field label="Module rate">
              <QNum w="w-28" prefix="$" value={line.matUnit} onChange={v => set({ matUnit: v })} />
            </Field>
          </div>
          <p className="mt-2 text-[11px] tabular-nums text-[var(--leon-black)]/60">
            Type rate <span className="text-[var(--leon-black)]/35">
              ({modules.toLocaleString()} modules &times; {fmtMoney(qnum(line.matUnit))})</span>{' '}
            <strong>{fmtMoney(typeRate)}</strong>
            {'  \u00b7  '}
            Vendor cost <span className="text-[var(--leon-black)]/35">
              (&times; {types.toLocaleString()} type{types === 1 ? '' : 's'})</span>{' '}
            <strong>{fmtMoney(vendorCost)}</strong>
          </p>
        </div>
      )}
    </div>
  );
}
function QuoteWizardScopeStep({ sec, qa, index, total, onChange, ctx }) {
  const byUnit = (sec.pricingMethod || 'rate') === 'unitPrice';
  const kindLabel = (QUOTE_SECTION_KINDS.find(k => k.key === sec.kind) || {}).label || sec.kind;
  const qsum = (sec.lines || []).reduce((a, l) => a + (l.excluded ? 0 : (l.qty || 0)), 0);
  return (
    <div className="space-y-3">
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] px-3 py-2">
        <div className="flex flex-wrap items-baseline gap-2">
          <span className="text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">
            Scope {index + 1} of {total}
          </span>
          <span className="text-sm font-bold">{sec.name}</span>
          <Badge tone="neutral">{kindLabel}</Badge>
          <div className="flex-1" />
          <span className="text-xs text-[var(--leon-black)]/55">
            {(sec.lines || []).length} line{(sec.lines || []).length === 1 ? '' : 's'} · {qty4(qsum)} {sec.uom}
          </span>
        </div>
      </div>

      {/* 1 · SALES INFO — what the scope sells for and what the sale costs. */}
      <QuoteBand id="wiz-sales" n="1" tone="sales" title="Sales info"
        right={`${pct(qnum(sec.ratePct == null ? qa.defaultRatePct : sec.ratePct))} margin`}>
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
        <Field label="How this scope is priced">
          <Select value={sec.pricingMethod || 'rate'} onChange={e => onChange({ pricingMethod: e.target.value })}>
            {QUOTE_PRICING_METHODS.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
          </Select>
        </Field>
        {(sec.pricingMethod === 'sell') ? (
          <Field label="Sell price" hint="What the client is being quoted. The margin is whatever it earns.">
            <QNum w="w-32" prefix="$" value={sec.sellPrice} onChange={v => onChange({ sellPrice: v })} />
          </Field>
        ) : byUnit ? (
          <Field label={`Unit price${sec.uom ? ` / ${sec.uom}` : ''}`} hint="The margin is whatever this earns.">
            <QNum w="w-28" prefix="$" value={sec.unitPrice} onChange={v => onChange({ unitPrice: v })} />
          </Field>
        ) : (
          <Field label="Margin" hint={sec.ratePct === null || sec.ratePct === undefined ? 'Following the job' : 'Set for this scope'}>
            <div className="flex items-center gap-1.5">
              <Select className="!w-28 !py-1 !text-xs"
                value={(quoteMarginTierFor(sec.ratePct) || {}).key || ''}
                onChange={e => {
                  const t = QUOTE_MARGIN_TIERS.find(x => x.key === e.target.value);
                  onChange({ ratePct: t ? t.rate : null });
                }}>
                <option value="">{sec.ratePct === null || sec.ratePct === undefined ? 'Follow job' : 'Custom'}</option>
                {QUOTE_MARGIN_TIERS.map(t => <option key={t.key} value={t.key}>{t.label}</option>)}
              </Select>
              <QPct w="w-20" value={sec.ratePct} onChange={v => onChange({ ratePct: v })} />
            </div>
          </Field>
        )}
        <Field label="UoM">
          <TextInput value={sec.uom || ''} onChange={e => onChange({ uom: e.target.value })} />
        </Field>
        {/* Overhead is charged on material BEFORE freight, so it is part of the
            cost the margin is taken over — it belongs beside the price, not in
            the logistics band. */}
        <Field label="Overhead" hint="On material, before freight.">
          <QPct w="w-20" placeholder={String(Math.round(qnum(qa.overheadPct) * 1000) / 10)}
            value={sec.overheadPct} onChange={v => onChange({ overheadPct: v })} />
        </Field>
        <Field label="What this scope counts" hint="What a container fraction is measured against.">
          <Select value={sec.costDriver || 'qty'} onChange={e => onChange({ costDriver: e.target.value })}>
            {QUOTE_COST_DRIVERS.map(d => <option key={d.key} value={d.key}>{d.label}</option>)}
          </Select>
        </Field>
        {(sec.costDriver === 'piece') && (
          <Field label="Stick length" hint="Inches. Linear feet become this many pieces.">
            <QNum w="w-24" value={sec.pieceLengthIn} onChange={v => onChange({ pieceLengthIn: v })} />
          </Field>
        )}
        <Field label="How material is bought">
          <Select value={sec.matBasis || 'unit'} onChange={e => onChange({ matBasis: e.target.value })}>
            {QUOTE_MATERIAL_BASES.map(b => <option key={b.key} value={b.key}>{b.label}</option>)}
          </Select>
        </Field>
        {(sec.matBasis || 'unit') === 'slab' && (
          <>
            <Field label="Yield per slab" hint="Usable units one slab gives.">
              <QNum w="w-24" value={sec.slabYield} onChange={v => onChange({ slabYield: v })} />
            </Field>
            <Field label="Waste" hint="Added before the slab count is worked out.">
              <QPct w="w-20" value={sec.slabWastePct} onChange={v => onChange({ slabWastePct: v })} />
            </Field>
            <Field label="Slab rate">
              <QNum w="w-28" prefix="$" value={sec.slabRate} onChange={v => onChange({ slabRate: v })} />
            </Field>
          </>
        )}
      </div>

      {/* The same three claims on the client price, from the same component the
          analysis screen uses — set a scope up here and review it there, and
          the two cannot say different things. */}
      <QuoteScopeCommissions sec={sec} qa={qa} set={onChange} editable />
      <QuoteScopeMargins st={quoteSectionTotals(sec, qa)} sec={sec} />
      </QuoteBand>

      {/* 2 · ADDITIONAL COSTS — what the goods cost to land. Labour ships
          nothing and clears no customs, so the band is only asked about where
          it means something. */}
      {sec.kind !== 'labor' && (
        <QuoteScopeLogistics sec={sec} qa={qa} set={onChange} editable ctx={ctx} />
      )}

      {/* Lines. A line READ from a take-off is shown and not edited — a wrong
          quantity is wrong in the take-off, which is the only place to correct
          it. A line added HERE is yours, and on a blank draft that is the only
          way the scope gets any, so it is fully editable. */}
      <Collapsible id={`wiz-lines-${quoteWizardKey(sec)}`} defaultOpen={!(sec.lines || []).length}
        title="3 · Item lines" count={(sec.lines || []).length}>
        {/* One sentence beats a legend: the tint is doing the work, this only
            has to name it. */}
        <p className="text-[11px] text-[var(--leon-black)]/50 mb-1.5">
          <span className="inline-block w-3 h-3 align-[-2px] mr-1 rounded-sm bg-[var(--leon-brown)]/20 border border-[var(--leon-brown)]/40" />
          The tinted columns are the ones to fill in &mdash; everything else is read off the take-off.
        </p>
        <div className="max-h-72 overflow-y-auto">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)] sticky top-0">
              <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
                <th className="px-2 py-1.5 min-w-[160px]">Item</th><th className="px-2 py-1.5">Area</th>
                <th className="px-2 py-1.5 text-right">Qty</th><th className="px-2 py-1.5">UoM</th>
                {/* Tinted because these are the cells to fill in — the rest
                    of the row is read off the take-off. */}
                <th className="px-2 py-1.5 text-right bg-[var(--leon-brown)]/20 text-[var(--leon-brown-dark)] border-l border-[var(--leon-brown)]/40">
                  {sec.kind === 'labor' ? 'Labour /u' : 'Mat /u'}
                </th>
                {/* Install is only ours on a combined package. A Supply Only
                    scope is not installed by us, so the column can only ever be
                    wrong — the line detail panel already had this rule and the
                    wizard did not. */}
                {sec.kind === 'combined' && (
                  <th className="px-2 py-1.5 text-right bg-[var(--leon-brown)]/20 text-[var(--leon-brown-dark)]">Install /u</th>
                )}
                <th className="px-2 py-1.5 w-6"></th>
              </tr>
            </thead>
            <tbody>
              {(sec.lines || []).map((l, i2) => {
                const editLine = f => onChange({
                  lines: (sec.lines || []).map((x, k) => (k === i2 ? { ...x, ...f } : x)),
                });
                const cell = (val, key, w) => (l.manual
                  ? <QNum w={w || 'w-20'} value={val} onChange={v => editLine({ [key]: v })} />
                  : <span className="tabular-nums">{key === 'qty' ? qty4(val) : (val ? fmtMoney(val) : '—')}</span>);
                // A break line or a note is one full-width editable row, drawn
                // the way the analysis screen draws it so the two read alike.
                if (!quoteRowIsItem(l)) {
                  const isArea = l.rowKind === 'area';
                  return (
                    <tr key={l.id || i2} className={isArea
                      ? 'border-t-2 border-b border-[var(--leon-brown)]/35 bg-[var(--leon-cream)]'
                      : 'border-b border-[var(--leon-line)]/60'}>
                      <td colSpan={sec.kind === 'combined' ? 7 : 6} className="px-2 py-1">
                        <div className="flex items-center gap-2">
                          <span className="text-[10px] text-[var(--leon-black)]/30" aria-hidden="true">
                            {isArea ? '\u25A6' : '\u270E'}
                          </span>
                          <input value={l.description || ''}
                            placeholder={isArea ? 'Which area these lines are for' : 'A note about the lines around this'}
                            onChange={e => editLine({ description: e.target.value })}
                            className={`q-cell flex-1 ${isArea
                              ? 'font-bold uppercase tracking-[0.1em] text-[11px]'
                              : 'italic text-[var(--leon-black)]/65'}`} />
                          <button type="button" title="Remove"
                            className="text-[var(--leon-black)]/30 hover:text-[var(--leon-red)]"
                            onClick={() => onChange({ lines: (sec.lines || []).filter((x, k) => k !== i2) })}>&#10005;</button>
                        </div>
                        {/* The specification for everything under this heading.
                            Asking it once per area rather than once per line is
                            the whole point of having areas. */}
                        {isArea && (
                          <div className="mt-1.5">
                            <QuoteAreaSpec ctx={ctx} sec={sec} area={l} set={editLine} />
                          </div>
                        )}
                      </td>
                    </tr>
                  );
                }
                return (
                  <React.Fragment key={l.id || i2}>
                  <tr className="border-t border-[var(--leon-line)]">
                    <td className="px-2 py-1">
                      {l.manual
                        ? <TextInput className="!py-0.5 !text-xs" value={l.description || ''}
                            onChange={e => editLine({ description: e.target.value })} placeholder="What it is" />
                        : (l.description || l.itemTag || '—')}
                    </td>
                    <td className="px-2 py-1 text-[var(--leon-black)]/55">
                      {l.manual
                        ? <TextInput className="!py-0.5 !text-xs !w-24" value={l.area || ''}
                            onChange={e => editLine({ area: e.target.value })} />
                        : (l.area || '—')}
                    </td>
                    <td className="px-2 py-1 text-right">{cell(l.qty, 'qty')}</td>
                    <td className="px-2 py-1 text-[var(--leon-black)]/55">
                      {l.manual
                        ? <TextInput className="!py-0.5 !text-xs !w-20" value={l.uom || ''}
                            onChange={e => editLine({ uom: e.target.value })} placeholder={sec.uom} />
                        : (l.uom || sec.uom)}
                    </td>
                    <td className="px-2 py-1 text-right bg-[var(--leon-brown)]/[0.07] border-l border-[var(--leon-brown)]/40">
                      {cell(sec.kind === 'labor' ? l.laborUnit : l.matUnit, sec.kind === 'labor' ? 'laborUnit' : 'matUnit')}
                    </td>
                    {sec.kind === 'combined' && (
                      <td className="px-2 py-1 text-right bg-[var(--leon-brown)]/[0.07]">{cell(l.installUnit, 'installUnit')}</td>
                    )}
                    <td className="px-2 py-1 text-right">
                      {l.manual && (
                        <button type="button" title="Remove this line"
                          className="text-[var(--leon-black)]/30 hover:text-[var(--leon-red)]"
                          onClick={() => onChange({ lines: (sec.lines || []).filter((x, k) => k !== i2) })}>&#10005;</button>
                      )}
                    </td>
                  </tr>
                  {/* The scope's own specification, under the line it belongs
                      to — the same fields and the same catalog picker the
                      analysis screen uses, so a scope set up here arrives there
                      already answered. */}
                  <tr className="border-t border-dashed border-[var(--leon-line)]">
                    <td colSpan={sec.kind === 'combined' ? 7 : 6} className="px-2 pb-2">
                      <QuoteWizardLineSpec ctx={ctx} sec={sec} line={l} set={editLine} />
                    </td>
                  </tr>
                  </React.Fragment>
                );
              })}
              {!(sec.lines || []).length && (
                <tr><td colSpan={7} className="px-2 py-4 text-center italic text-[var(--leon-black)]/40">
                  No lines yet — add them here.
                </td></tr>
              )}
            </tbody>
          </table>
        </div>
        <div className="flex flex-wrap items-center gap-3 mt-2">
          <button type="button" className="text-xs font-semibold text-[var(--leon-brown)]"
            onClick={() => onChange({
              lines: (sec.lines || []).concat([makeQuoteLine({ manual: true, uom: sec.uom || '' })]),
            })}>+ Add a line</button>
          {/* A break line and a note line are rows in the same list, so they
              order WITH the work instead of collecting at the bottom. They were
              on the analysis screen and not here, which is half a feature. */}
          {QUOTE_ROW_KINDS.map(k => (
            <button key={k.key} type="button" title={k.hint}
              className="text-xs font-semibold text-[var(--leon-black)]/50 hover:text-[var(--leon-brown)]"
              onClick={() => onChange({
                lines: (sec.lines || []).concat([makeQuoteLine({ manual: true, rowKind: k.key })]),
              })}>+ Add {k.label === 'area' ? 'an' : 'a'} {k.label}</button>
          ))}
          {(sec.lines || []).some(l => !l.manual) && (
            <span className="text-[11px] text-[var(--leon-black)]/50 basis-full">
              Lines read from the take-off keep their quantities — a wrong quantity is wrong in the
              take-off, which is the only place it should be corrected.
            </span>
          )}
        </div>
      </Collapsible>
    </div>
  );
}
// `editQa` puts the wizard on an EXISTING revision instead of building a new
// one. It skips Source and Scopes — the scopes are already chosen and their
// lines already exist, and re-running the supply/labour split over them would
// duplicate every scope — and runs Sales points › each scope › Review, writing
// back to that revision on Save. Adding or removing a scope stays on the
// analysis screen, where the whole list is visible at once.
function GenerateQuoteAnalysisModal({ open, onClose, ctx, project, onCreated, editQa }) {
  const [source, setSource] = useState('takeoff');
  const [takeoffId, setTakeoffId] = useState('');
  const [name, setName] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');
  const [preview, setPreview] = useState(null);   // { sections, warnings, label }
  const fileRef = useRef(null);
  // 'source' | 'scopes' | <index into the built sections> | 'review'
  const [step, setStep] = useState('source');
  // Two independent layers plus the combined package. Derived into the single
  // choice map buildQuoteSections takes, so the engine keeps one shape while
  // the screen asks the question the way it is actually decided.
  const [layers, setLayers] = useState({ supply: {}, labor: {}, combined: {} });
  // Keyed by WHAT the scope is, never by its position. `built` is regenerated
  // from scratch whenever the scope selection changes — with fresh ids each
  // time — so an index-keyed map silently reassigns scope 1's lines and margin
  // onto whatever scope moved into slot 0. That is exactly what happened:
  // untick Casework and the line typed against it reappeared under Doors.
  const [overrides, setOverrides] = useState({});// quoteWizardKey -> fields
  const [newWindow, setNewWindow] = useState(false);
  // The job-level sales points. They used to sit as thirteen live boxes above
  // the line table, repricing every scope the moment one was touched; they are
  // asked here instead, once, as their own step.
  const [job, setJob] = useState(() => quoteJobFields(makeQuoteAnalysis({})));
  const editing = !!editQa;

  const interiorTakeoffs = (project.takeOffs || []).filter(t => t.department !== 'Windows');
  // What the company SELLS, which is what a quote is written against. The job's
  // own scopes do not exist yet at quote time — they are created from the
  // quotation when it is converted — so seeding from them gave nothing.
  const sellable = familiesForRegion(ctx.scopeLibrary, (ctx.accounts.find(a => a.id === project.accountId) || {}).region)
    .filter(f => familyDepartment(f.name, ctx.scopeLibrary) !== 'Windows');

  useEffect(() => {
    if (!open) return;
    setSource('takeoff'); setTakeoffId(''); setError(''); setPreview(null); setBusy(false);
    setLayers({ supply: {}, labor: {}, combined: {} });
    setOverrides({}); setNewWindow(false);
    if (editQa) {
      // Straight to the scopes: this revision already has them, and the sales
      // information now lives on each scope rather than in a step of its own.
      setStep(0); setName(editQa.name || '');
      setJob(quoteJobFields(editQa));
    } else {
      setStep('source'); setName(`Quote Analysis — ${project.name}`);
      setJob(quoteJobFields(makeQuoteAnalysis({})));
    }
  }, [open, project.name, editQa && editQa.id]);

  async function readWorkbook(buffer, label) {
    setBusy(true); setError('');
    try {
      const res = parseTakeoffWorkbook(buffer);
      if (!res.sections.length) {
        setError(res.warnings[0] || 'No take-off tables were found in that workbook.');
        setPreview(null);
      } else {
        setPreview({ ...res, label });
        setStep('scopes');
      }
    } catch (e) {
      setError(e.message || 'That file could not be read as a take-off workbook.');
      setPreview(null);
    }
    setBusy(false);
  }

  function useTakeoff() {
    const t = interiorTakeoffs.find(x => x.id === takeoffId);
    if (!t) { setError('Pick a take-off first.'); return; }
    if (!t.fileUrl) {
      setError(`"${t.name}" has no file attached, so there is nothing to read. Attach the workbook in the Take-offs Hub, or upload it here instead.`);
      return;
    }
    if (!/\.xlsx?$/i.test(t.name || '')) {
      setError(`"${t.name}" is not an Excel workbook. Only .xlsx take-offs can be read.`);
      return;
    }
    readWorkbook(dataUrlToArrayBuffer(t.fileUrl), t.name);
  }

  function onFile(e) {
    const f = e.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => readWorkbook(reader.result, f.name);
    reader.onerror = () => setError('Could not read that file.');
    reader.readAsArrayBuffer(f);
    e.target.value = '';
  }

  function commit(sections, warnings, src, ref) {
    if (editQa) {
      ctx.updateQuoteAnalysis(project.id, editQa.id,
        Object.assign({ name: name.trim() || editQa.name, sections }, job));
      onCreated(editQa.id, false);
      return;
    }
    const id = ctx.createQuoteAnalysis(project.id, Object.assign({
      name: name.trim() || `Quote Analysis — ${project.name}`,
      source: src, sourceRef: ref || '', sections, warnings: warnings || [],
    }, job));
    onCreated(id, newWindow);
  }

  // ── the wizard ────────────────────────────────────────────────────────────
  // One kind of question at a time, over the whole draft: where the quantities
  // come from, then how each scope is sold, then each scope on its own. This is
  // the shape CounterGo's six-step editor uses and the reason a salesperson who
  // is not an estimator can finish a quote — the alternative is one screen with
  // forty fields on it.
  const kinds = useMemo(() => {
    const out = {};
    (preview ? preview.sections : []).forEach(sc => {
      const key = sc.scopeKey || sc.name;
      if (quoteScopeIsCombined(sc.name) || quoteScopeIsCombined(sc.scopeKey)) {
        out[key] = (layers.combined || {})[key] ? 'combined' : 'skip';
        return;
      }
      const sup = !!(layers.supply || {})[key];
      const lab = !!(layers.labor || {})[key];
      out[key] = sup && lab ? 'both' : sup ? 'supply' : lab ? 'labor' : 'skip';
    });
    return out;
  }, [preview, layers]);
  const built = useMemo(
    () => (editQa ? (editQa.sections || [])
                  : preview ? buildQuoteSections(preview.sections, kinds) : []),
    [editQa, preview, kinds]);
  const stepScope = typeof step === 'number' ? built[step] : null;

  function patchBuilt(sec, fields) {
    // The wizard edits its own copy; nothing reaches the project until Create.
    const k = quoteWizardKey(sec);
    setOverrides(o => ({ ...o, [k]: { ...(o[k] || {}), ...fields } }));
  }
  const finalSections = built.map(sec => ({ ...sec, ...(overrides[quoteWizardKey(sec)] || {}) }));

  // A wizard that swaps its whole body while you are looking at the footer has
  // to take you back to the top of the new step, or the change is invisible.
  const bodyRef = useRef(null);
  useEffect(() => {
    const box = bodyRef.current && bodyRef.current.closest('.overflow-y-auto');
    if (box) box.scrollTop = 0;
  }, [step]);

  return (
    <Modal open={open} onClose={onClose} size="xl"
      title={editing ? `Quote Analysis — Rev ${editQa.revision || 1}` : 'Generate Quote Analysis'}
      footer={
        <div className="flex items-center justify-between gap-3 w-full">
          <div className="text-xs text-[var(--leon-black)]/50">
            {editing
              ? 'Editing this revision only — no scopes, no schedule, no contract value.'
              : 'This creates a pricing draft only — no scopes, no schedule, no contract value.'}
          </div>
          <div className="flex gap-2 items-center">
            {step !== 'source' && !(editing && step === 0) && (
              <Button variant="ghost" onClick={() => {
                if (step === 'scopes') setStep('source');
                else if (step === 'review') setStep(built.length ? built.length - 1 : 'scopes');
                else if (step === 0) setStep('scopes');
                else setStep(step - 1);
              }}>← Back</Button>
            )}
            <Button variant="ghost" onClick={onClose}>Cancel</Button>
            {step === 'source' && (
              source === 'takeoff' ? <Button onClick={useTakeoff} disabled={busy || !takeoffId}>{busy ? 'Reading…' : 'Read take-off'}</Button>
              : source === 'excel' ? <Button onClick={() => fileRef.current.click()} disabled={busy}>{busy ? 'Reading…' : 'Choose workbook…'}</Button>
              : <Button onClick={() => { setPreview({ sections: quoteSectionsFromFamilies(sellable), warnings: [], label: '' }); setStep('scopes'); }}>
                  Next — choose scopes
                </Button>
            )}
            {step === 'scopes' && (
              <Button disabled={!built.length}
                onClick={() => setStep(built.length ? 0 : 'review')}>
                {built.length ? `Next — ${built.length} scope${built.length === 1 ? '' : 's'} to price` : 'Choose at least one'}
              </Button>
            )}
            {typeof step === 'number' && (
              <Button onClick={() => setStep(step + 1 < built.length ? step + 1 : 'review')}>
                {step + 1 < built.length ? 'Next scope →' : 'Next — review'}
              </Button>
            )}
            {step === 'review' && (
              <Button onClick={() => commit(finalSections, (preview && preview.warnings) || [],
                source === 'excel' ? 'excel' : source === 'takeoff' ? 'takeoff' : 'manual',
                (preview && preview.label) || '')}>
                {editing ? `Save Rev ${editQa.revision || 1}` : 'Create the analysis'}
              </Button>
            )}
          </div>
        </div>
      }>
      <div className="space-y-4" ref={bodyRef}>
        {/* Where you are, so a wizard never feels like it lost you. */}
        <div className="flex items-center gap-1.5 text-[11px] font-semibold">
          {(editing
            ? [['price', 'Price each'], ['review', 'Review']]
            : [['source', 'Source'], ['scopes', 'Scopes'],
               ['price', 'Price each'], ['review', 'Review']]).map(([k, label], i2, arr) => {
            const order = arr.map(x => x[0]);
            const here = typeof step === 'number' ? 'price' : step;
            const at = k === 'price' ? typeof step === 'number' : step === k;
            const done = order.indexOf(k) < order.indexOf(here);
            return (
              <React.Fragment key={k}>
                {i2 > 0 && <span aria-hidden="true" className="text-[var(--leon-black)]/20">›</span>}
                <span className={`px-2 py-0.5 rounded ${at ? 'bg-[var(--leon-black)] text-white'
                  : done ? 'text-[var(--leon-brown)]' : 'text-[var(--leon-black)]/35'}`}>
                  {label}{k === 'price' && built.length ? ` (${typeof step === 'number' ? step + 1 : built.length}/${built.length})` : ''}
                </span>
              </React.Fragment>
            );
          })}
        </div>

        {step === 'source' && (<>
        <Field label="Name">
          <TextInput value={name} onChange={e => setName(e.target.value)} />
        </Field>

        <div>
          <span className="block text-xs font-semibold text-[var(--leon-black)]/60 uppercase tracking-wide mb-2">Where the quantities come from</span>
          <div className="grid gap-2 md:grid-cols-3">
            {[
              { key: 'takeoff', icon: '📏', label: 'A take-off on this job', hint: `${interiorTakeoffs.length} filed` },
              { key: 'excel', icon: '📊', label: 'Upload an Excel take-off', hint: 'LEON Take-Off Template' },
              { key: 'blank', icon: '✏️', label: 'Start blank', hint: `${sellable.length} scope${sellable.length === 1 ? '' : 's'} we sell` },
            ].map(o => (
              <button key={o.key} type="button" onClick={() => { setSource(o.key); setPreview(null); setError(''); }}
                className={`text-left rounded-lg border p-3 transition ${source === o.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                <div className="text-xl mb-1">{o.icon}</div>
                <div className="font-semibold text-sm">{o.label}</div>
                <div className="text-[11px] text-[var(--leon-black)]/50">{o.hint}</div>
              </button>
            ))}
          </div>
        </div>

        {source === 'takeoff' && (
          <Field label="Take-off" hint="Only take-offs with an .xlsx attached can be read.">
            <Select value={takeoffId} onChange={e => { setTakeoffId(e.target.value); setPreview(null); setError(''); }}>
              <option value="">Select a take-off…</option>
              {interiorTakeoffs.map(t => (
                <option key={t.id} value={t.id}>
                  {t.name} — Rev {t.revision}{t.fileUrl ? '' : ' (no file attached)'}
                </option>
              ))}
            </Select>
          </Field>
        )}
        {source === 'excel' && (
          <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-4 text-sm text-[var(--leon-black)]/60">
            Reads the LEON Take-Off Template — one tab per scope. Columns are matched by name, so a
            workbook saved from an earlier version of the template still imports correctly.
            Quantities are recalculated from Qty per Unit, the Unit Matrix and the waste allowance
            rather than read from the cells, so a template that has never been opened in Excel still
            imports real numbers.
            <input ref={fileRef} type="file" accept=".xlsx,.xls" className="hidden" onChange={onFile} />
          </div>
        )}
        {source === 'blank' && (
          <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-4 text-sm text-[var(--leon-black)]/60">
            Lists every scope the company sells, so you choose what this job is being quoted for and
            price it line by line. The job itself has no scopes yet &mdash; those are created from this
            quotation when it is converted to a contract.
          </div>
        )}

        {error && <div className="rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm p-3">{error}</div>}

        </>)}

        {/* Step 2 — TWO LAYERS, chosen independently. Supply and labour are not
            the same list: a job can be supplied without being installed, and
            installed without being supplied. One dropdown per scope forced them
            to match, which is a decision the software was making rather than
            the person quoting. Countertops sit outside both — one contract buys
            the slab, templates it and installs it. */}
        {step === 'scopes' && preview && (
          <div className="space-y-3">
            <p className="text-xs text-[var(--leon-black)]/60">
              {preview.label ? `Read from ${preview.label}. ` : ''}
              Tick a scope under <strong>Supply</strong>, under <strong>Labor</strong>, or under both.
              They become separate scopes and are priced completely separately &mdash; their own margin,
              their own cost recipe, their own commission.
            </p>
            <div className="grid md:grid-cols-2 gap-3">
              {[
                { key: 'supply', label: 'Supply', icon: '📦', hint: 'Material delivered. Carries freight and duty.' },
                { key: 'labor', label: 'Labor', icon: '🔨', hint: 'Installation only. No freight, no duty.' },
              ].map(layer => (
                <div key={layer.key} className="rounded-lg border border-[var(--leon-line)] overflow-hidden">
                  <div className="px-3 py-2 bg-[var(--leon-cream)]">
                    <div className="text-sm font-bold">
                      <span aria-hidden="true" className="mr-1.5">{layer.icon}</span>{layer.label}
                    </div>
                    <div className="text-[11px] text-[var(--leon-black)]/55">{layer.hint}</div>
                  </div>
                  <div className="max-h-64 overflow-y-auto divide-y divide-[var(--leon-line)]">
                    {preview.sections.filter(sc => !quoteScopeIsCombined(sc.name) && !quoteScopeIsCombined(sc.scopeKey)).map(sc => {
                      const key = sc.scopeKey || sc.name;
                      const on = !!(layers[layer.key] || {})[key];
                      const qsum = sc.lines.reduce((a, l) => a + (l.excluded ? 0 : (l.qty || 0)), 0);
                      return (
                        <label key={sc.id} className="flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer hover:bg-[var(--leon-cream)]/60">
                          <input type="checkbox" checked={on}
                            onChange={e => setLayers(L => ({ ...L,
                              [layer.key]: { ...(L[layer.key] || {}), [key]: e.target.checked } }))} />
                          <span className="flex-1 min-w-0 truncate">{sc.name}</span>
                          {!!qsum && <span className="text-[11px] text-[var(--leon-black)]/45 tabular-nums">{qty4(qsum)} {sc.uom}</span>}
                        </label>
                      );
                    })}
                    {!preview.sections.filter(sc => !quoteScopeIsCombined(sc.name) && !quoteScopeIsCombined(sc.scopeKey)).length && (
                      <p className="px-3 py-4 text-xs italic text-[var(--leon-black)]/40">Nothing to choose here.</p>
                    )}
                  </div>
                </div>
              ))}
            </div>

            {/* The one package that is both, so it belongs to neither layer. */}
            {!!preview.sections.filter(sc => quoteScopeIsCombined(sc.name) || quoteScopeIsCombined(sc.scopeKey)).length && (
              <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden">
                <div className="px-3 py-2 bg-[var(--leon-cream)]">
                  <div className="text-sm font-bold"><span aria-hidden="true" className="mr-1.5">🪨</span>Supply &amp; Install</div>
                  <div className="text-[11px] text-[var(--leon-black)]/55">
                    One contract buys the slab, templates it and installs it — so it is not split.
                  </div>
                </div>
                <div className="divide-y divide-[var(--leon-line)]">
                  {preview.sections.filter(sc => quoteScopeIsCombined(sc.name) || quoteScopeIsCombined(sc.scopeKey)).map(sc => {
                    const key = sc.scopeKey || sc.name;
                    return (
                      <label key={sc.id} className="flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer hover:bg-[var(--leon-cream)]/60">
                        <input type="checkbox" checked={!!(layers.combined || {})[key]}
                          onChange={e => setLayers(L => ({ ...L, combined: { ...(L.combined || {}), [key]: e.target.checked } }))} />
                        <span className="flex-1">{sc.name}</span>
                      </label>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        )}

        {/* Step 3..N — one scope at a time. The job's own sales points are
            handed down, so a scope that says "following the job" shows the
            figure it will actually follow. This used to be a hardcoded literal
            — 40% margin, 10% overhead — that matched nothing. */}
        {typeof step === 'number' && stepScope && (
          <QuoteWizardScopeStep ctx={ctx} key={quoteWizardKey(stepScope)}
            sec={{ ...stepScope, ...(overrides[quoteWizardKey(stepScope)] || {}) }} qa={job}
            index={step} total={built.length} onChange={f => patchBuilt(stepScope, f)} />
        )}

        {/* Step 4 — what it all adds up to, before anything is written. */}
        {step === 'review' && (
          <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden">
            <div className="px-3 py-2 bg-[var(--leon-cream)] font-semibold text-sm">
              {editing
                ? <>Nothing is written until you save &mdash; this is what Rev {editQa.revision || 1} will hold.</>
                : <>Nothing has been created yet &mdash; this is what will be.</>}
            </div>
            <table className="w-full text-sm">
              <thead><tr className="text-left text-[11px] uppercase tracking-wide text-[var(--leon-black)]/50">
                <th className="px-3 py-1.5">Scope</th><th className="px-3 py-1.5 text-right">Lines</th>
                <th className="px-3 py-1.5 text-right">Total qty</th><th className="px-3 py-1.5">UoM</th>
              </tr></thead>
              <tbody>
                {finalSections.map((sc, i2) => {
                  const q = sc.lines.reduce((a, l) => a + (l.excluded ? 0 : (l.qty || 0)), 0);
                  return (
                    <tr key={sc.id || i2} className="border-t border-[var(--leon-line)]">
                      <td className="px-3 py-1.5 font-medium">{sc.name}</td>
                      <td className="px-3 py-1.5 text-right">{sc.lines.length}</td>
                      <td className="px-3 py-1.5 text-right tabular-nums">{qty4(q)}</td>
                      <td className="px-3 py-1.5 text-[var(--leon-black)]/60">{sc.uom}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
            {!editing && (
              <label className="flex items-center gap-2 px-3 py-2 border-t border-[var(--leon-line)] text-xs">
                <input type="checkbox" checked={newWindow} onChange={e => setNewWindow(e.target.checked)} />
                Open the new draft in its own browser window
              </label>
            )}
            {!!(preview && preview.warnings && preview.warnings.length) && (
              <div className="px-3 py-2 border-t border-[var(--leon-line)] bg-amber-50 text-[12px] text-amber-900">
                <div className="font-semibold mb-1">{preview.warnings.length} thing{preview.warnings.length === 1 ? '' : 's'} to check</div>
                <ul className="list-disc pl-4 space-y-0.5 max-h-32 overflow-y-auto">
                  {preview.warnings.slice(0, 25).map((w, i) => <li key={i}>{w}</li>)}
                </ul>
                {preview.warnings.length > 25 && <div className="mt-1 opacity-70">…and {preview.warnings.length - 25} more, kept on the draft.</div>}
              </div>
            )}
          </div>
        )}
      </div>
    </Modal>
  );
}

function QuoteAnalysisDetail({ ctx, project, qa, onBack, onOpen, editable, startInWizard, onWizardDone, onRevised }) {
  const [addingScope, setAddingScope] = useState(false);
  const [newScope, setNewScope] = useState('');
  const [newScopeKind, setNewScopeKind] = useState('both');
  const [sub, setSub] = useState('overview');
  const [converting, setConverting] = useState(false);
  // The wizard, reopened on THIS revision. A quotation's job-level dials are
  // answered there, not as live boxes over the totals.
  const [inWizard, setInWizard] = useState(!!startInWizard);
  useEffect(() => { if (startInWizard) setInWizard(true); }, [startInWizard, qa.id]);
  const onReviseInWizard = () => setInWizard(true);
  // 'essentials' | 'full' — how many cost columns the line tables show.
  const [density, setDensity] = useState('essentials');
  // A combined scope belongs to both views — it is one contract covering both.
  const shown = kind => (qa.sections || []).filter(s => s.kind === kind || s.kind === 'combined');
// A quote analysis into LEON Sheets, the same way a bill of quantities goes. Both
// are tables someone will want to work on further, and a CSV in Downloads is
// the one place a number stops being connected to the job.
function quoteDraftToSheet(ctx, project, qa) {
  if (typeof ctx.exportToSheet !== 'function') return null;
  const money = '#,##0.00';
  const head = ['Scope', 'Sold as', 'Item', 'Description', 'Qty', 'Unit',
                'Material/unit', 'Labour/unit', 'Install/unit', 'Freight', 'Duty',
                'Cost', 'Sell', 'Margin', 'Margin %'];
  const rows = [head.map(h => ({ v: h, bold: true }))];
  (qa.sections || []).forEach(sec => {
    (sec.lines || []).forEach(l => {
      const lt = quoteLineTotals(l, qa, sec);
      rows.push([
        sec.name || sec.familyName || '', sec.kind || '', l.code || '', l.description || '',
        Number(lt.qty) || 0, l.unit || '',
        { v: Number(l.matUnit) || 0, fmt: money }, { v: Number(l.laborUnit) || 0, fmt: money },
        { v: Number(l.installUnit) || 0, fmt: money }, { v: lt.freight, fmt: money },
        { v: lt.duty, fmt: money }, { v: lt.cost, fmt: money }, { v: lt.sell, fmt: money },
        { v: lt.margin, fmt: money }, { v: (lt.marginPct || 0) / 100, fmt: '0.0%' },
      ]);
    });
  });
  const t = quoteAnalysisTotals(qa);
  rows.push([]);
  rows.push(['TOTAL', '', '', '', '', '', '', '', '', '', '',
             { v: t.cost, bold: true, fmt: money }, { v: t.sell, bold: true, fmt: money },
             { v: t.sell - t.cost, bold: true, fmt: money },
             { v: (t.marginPct || 0) / 100, bold: true, fmt: '0.0%' }].map((c, i) =>
             (i === 0 ? { v: 'TOTAL', bold: true } : c)));
  return ctx.exportToSheet({
    name: `${project.name} — ${qa.name} Rev ${qa.revision}`,
    sheetName: 'Quote analysis', projectId: project.id, source: 'a quote analysis',
    rows, colWidths: [22, 12, 12, 34, 9, 8, 13, 12, 12, 11, 10, 12, 12, 12, 10],
  });
}

  const t = quoteAnalysisTotals(qa);
  const set = f => ctx.updateQuoteAnalysis(project.id, qa.id, f);
  // Both frozen states, for the same reason: an issued revision is what was
  // quoted, and a superseded one is what was quoted before that. Neither is
  // editable — editing forks the next revision instead.
  const locked = qa.status === 'Superseded' || qa.status === 'Issued';
  const canEdit = editable && !locked;

  return (
    <div className="space-y-4" data-print-region="Quote Analysis">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <button onClick={onBack} className="text-sm font-semibold text-[var(--leon-brown)]">← All quote analyses</button>
        <div className="flex items-center gap-2">
          <DocActions title={`${qa.name} Rev ${qa.revision}`} heading="Quote Analysis"
            lines={[project.name, `Rev ${qa.revision} · ${qa.status}`, `Prepared by ${qa.preparedBy || '—'}`]} />
          <ShareButton ctx={ctx} projectId={project.id} subjectKey={`quoteAnalysis:${qa.id}`}
            subject={`${qa.name} — Rev ${qa.revision}`}
            summary={`${t.sections} scopes · ${fmtMoney(t.sell)} · ${pct(t.marginPct)} margin`} />
          <Button variant="ghost" size="sm" onClick={() => {
            const d = quoteDraftToSheet(ctx, project, qa);
            if (d) alert('Sent to LEON Sheets as "' + d.name + '". Open it under LEON Studio → LEON Sheets.');
          }}>Send to LEON Sheets</Button>
          {/* Done editing: freeze THIS one as the revision. The next edit forks
              Rev N+1 rather than rewriting what was already quoted. */}
          {canEdit && (
            <Button size="sm" onClick={() => {
              if (confirm(`Save "${qa.name}" as Rev ${qa.revision}?\n\nIt becomes read-only. Editing it afterwards starts Rev ${(qa.revision || 1) + 1}.`))
                ctx.finalizeQuoteAnalysis(project.id, qa.id);
            }}>Save as Rev {qa.revision}</Button>
          )}
          {/* The step across from internal working to a client document. After
              this the deck is arranged and issued under Quotes; the analysis
              goes back to being the pricing behind it. */}
          {canEdit && (() => {
            const made = (project.quoteRevisions || []).find(q =>
              q.quoteAnalysisId === qa.id && q.analysisRevision === qa.revision);
            return made
              ? <Badge tone="green">Quotation Rev {made.revision} created</Badge>
              : <Button size="sm" variant="primary" onClick={() => {
                  if (confirm(`Create a quotation from "${qa.name}" Rev ${qa.revision}?\n\nIt is filed under Quotes, where the deck is arranged and issued.`))
                    ctx.createQuoteFromAnalysis(project.id, qa.id);
                }}>Create quotation →</Button>;
          })()}
          {/* Rehomed from the removed sales-points strip. Both act on the
              whole quotation, so they belong with the quotation's own actions
              rather than above the scopes. */}
          {canEdit && !!(qa.sections || []).length && (() => {
            const differing = (qa.sections || []).filter(x => x.ratePct !== null && x.ratePct !== undefined);
            return (
              <Button size="sm" variant="ghost"
                title={differing.length
                  ? `${differing.map(x => x.name).join(', ')} price at their own rate`
                  : 'Every scope already follows the job'}
                onClick={() => {
                  if (!differing.length) { alert('Every scope already follows the job\u2019s margin.'); return; }
                  const list = differing.map(x => `\u2022 ${x.name} — ${pct(x.ratePct)}`).join('\n');
                  if (confirm(`Put every scope back on the job\u2019s ${pct(qa.defaultRatePct)} margin?\n\nThese ${differing.length} price at their own rate and will follow the job instead:\n\n${list}`)) {
                    ctx.applyQuoteMarginToAll(project.id, qa.id);
                  }
                }}>
                Apply margin to all{differing.length ? ` (${differing.length} differ)` : ''}
              </Button>
            );
          })()}
          {canEdit && onReviseInWizard && (
            <Button size="sm" variant="ghost" onClick={onReviseInWizard}>Open the wizard →</Button>
          )}
          {editable && <Button variant="ghost" size="sm" onClick={() => {
            if (confirm(`Delete "${qa.name}" (Rev ${qa.revision})? This cannot be undone.`)) {
              ctx.removeQuoteAnalysis(project.id, qa.id); onBack();
            }
          }}>Delete</Button>}
        </div>
      </div>

      {qa.convertedDate && (
        <div className="rounded-lg border border-[var(--leon-brown)]/40 bg-[var(--leon-cream)] p-3 text-sm">
          <strong>Converted to contract</strong> on {fmtDate(qa.convertedDate)}
          {qa.convertedBy ? ` by ${qa.convertedBy}` : ''} &mdash; the scopes and their schedules are on
          Schedule by Stages. This quotation is unchanged and stays the record of what was agreed.
        </div>
      )}
      {locked && (
        <div className="rounded-lg bg-[var(--leon-line)]/40 border border-[var(--leon-line)] p-3 text-sm flex flex-wrap items-center gap-3">
          <span>
            {qa.status === 'Issued'
              ? <>Saved as <strong>Rev {qa.revision}</strong>{qa.issuedDate ? ` on ${fmtDate(qa.issuedDate)}` : ''}
                  {qa.issuedBy ? ` by ${qa.issuedBy}` : ''}. It is read-only, which is what makes it a record
                  of what was quoted.</>
              : <>This revision is <strong>Superseded</strong> and is kept read-only, exactly as it was sent.</>}
          </span>
          <div className="flex-1" />
          {editable && qa.status === 'Issued' && !qa.convertedDate && (
            <Button size="sm" variant="ghost" onClick={() => {
              // Open the new revision rather than dropping back to the list —
              // you pressed Edit because you want to edit it.
              const id = ctx.reviseQuoteAnalysis(project.id, qa.id);
              // Straight back into the wizard on the new revision — the client
              // asked for the guided flow every time a revision is raised.
              if (id) { if (onRevised) onRevised(id); else if (onOpen) onOpen(id); else onBack(); }
            }}>Edit as Rev {(qa.revision || 1) + 1}</Button>
          )}
          {/* The one step that turns a bid into work. Only on an ISSUED
              revision — you do not contract a draft — and only once. */}
          {editable && qa.status === 'Issued' && !qa.convertedDate && (
            <Button size="sm" onClick={() => setConverting(true)}>Convert to contract →</Button>
          )}
        </div>
      )}

      {/* Header — the job, the version, and the totals it adds up to */}
      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
        <div className="p-4 grid gap-3 md:grid-cols-4">
          <Field label="Name" className="md:col-span-2">
            <TextInput value={qa.name} disabled={!canEdit} onChange={e => set({ name: e.target.value })} />
          </Field>
          {/* Two holes lived in this one control, and they defeated the whole
              point of freezing a revision:
                1. It was gated on `editable` — the PERMISSION — not on
                   `locked`, so an Issued revision could be set back to Draft
                   from a dropdown and edited, and the version the client is
                   holding silently became editable again.
                2. Picking "Issued" here set the status WITHOUT stamping the
                   date and the person, so a revision could read as issued with
                   no record of when or by whom.
              Issuing is an ACT, not a value: it happens through "Issue as
              Rev N" (on the Client Quote tab, and offered on the way out when
              you send), which stamps both. What is left here is the one real
              choice a draft has — whether it is still being written or is with
              someone for review. */}
          <Field label="Status"
            hint={locked ? 'Set by issuing, and not editable afterwards.' : undefined}>
            {locked ? (
              <div className="flex items-center gap-2 py-1">
                <Badge tone={qa.status === 'Issued' ? 'green' : 'neutral'}>{qa.status}</Badge>
                <span className="text-xs text-[var(--leon-black)]/50">
                  {qa.status === 'Issued'
                    ? `${qa.issuedDate ? fmtDate(qa.issuedDate) : ''}${qa.issuedBy ? ` · ${qa.issuedBy}` : ''}`
                    : 'replaced by a later revision'}
                </span>
              </div>
            ) : (
              <Select value={qa.status} disabled={!canEdit} onChange={e => set({ status: e.target.value })}>
                {QUOTE_ANALYSIS_DRAFT_STATUSES.map(s => <option key={s}>{s}</option>)}
              </Select>
            )}
          </Field>
          <Field label="Prepared by">
            <TextInput value={qa.preparedBy} disabled={!canEdit} onChange={e => set({ preparedBy: e.target.value })} />
          </Field>
        </div>

        {/* The job-level sales points USED TO BE STATED HERE, as a strip above
            the scopes. Removed on the client's instruction: sales information
            belongs to each scope, not to a block you read before reaching any
            of them. Every figure it showed is now asked inside that scope's own
            "1 · Sales info", where the scope it prices is on screen beside it.
            The two ACTIONS it carried were real and moved to the action row. */}
        <GenerateQuoteAnalysisModal open={inWizard} editQa={qa} ctx={ctx} project={project}
          onClose={() => { setInWizard(false); if (onWizardDone) onWizardDone(); }}
          onCreated={() => { setInWizard(false); if (onWizardDone) onWizardDone(); }} />

        <div className="grid grid-cols-2 md:grid-cols-5 divide-x divide-[var(--leon-line)] border-t border-[var(--leon-line)] bg-[var(--leon-cream)]">
          {[
            ['Material', fmtMoney(t.mat)], ['Labour + install', fmtMoney(t.labor + t.install)],
            ['Freight + duty', fmtMoney(t.freight + t.duty + t.overhead)], ['Total cost', fmtMoney(t.cost)],
          ].map(([k, v]) => (
            <div key={k} className="p-3 text-center">
              <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{k}</div>
              <div className="font-semibold">{v}</div>
            </div>
          ))}
          <div className="p-3 text-center bg-[var(--leon-brown)] text-white">
            <div className="text-[10px] uppercase tracking-wide opacity-70">Sell</div>
            <div className="font-bold text-lg">{fmtMoney(t.sell)}</div>
            <div className="text-[11px] opacity-80">{fmtMoney(t.profit)} profit · {pct(t.cm)} CM</div>
          </div>
        </div>
      </div>

      {!!(qa.warnings || []).length && (
        <Collapsible id={`qa-warn-${qa.id}`} title="Import notes" count={qa.warnings.length}>
          <ul className="list-disc pl-5 space-y-1 text-sm text-[var(--leon-black)]/70">
            {qa.warnings.map((w, i) => <li key={i}>{w}</li>)}
          </ul>
        </Collapsible>
      )}

      {/* Supply and labour are separate contracts, so they are separate views.
          A combined scope (countertops) appears under both, because it is one
          contract that genuinely covers both. */}
      <div className="flex gap-1 border-b border-[var(--leon-line)]">
        {[
          { key: 'overview', label: 'Overview', icon: '📊' },
          { key: 'supply', label: 'Supply', icon: '📦' },
          { key: 'labor', label: 'Labor', icon: '🔨' },

        ].map(t => {
          const n = t.key === 'overview' ? 0 : shown(t.key).length;
          return (
            <button key={t.key} onClick={() => setSub(t.key)}
              className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
              <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>{t.label}
              {t.key !== 'overview' && <span className="ml-1.5 opacity-50">{n}</span>}
            </button>
          );
        })}
      </div>

      {sub === 'overview' && <QuoteOverviewPanel qa={qa} />}


      {/* Every cost element as its own column runs to twenty columns, which no
          page width can line up — and a table that scrolls sideways is the one
          the reviewer stops trusting. Essentials is the default: what the item
          is, what it costs, what it sells for. The breakdown is one click away
          on the whole draft, and each line's full arithmetic is always in its
          own detail panel regardless. */}
      {(sub === 'supply' || sub === 'labor') && !!shown(sub).length && (
        <div className="flex items-center gap-2 text-xs">
          <span className="text-[var(--leon-black)]/45 uppercase tracking-wide text-[10px]">Columns</span>
          <div className="inline-flex rounded border border-[var(--leon-line)] overflow-hidden">
            {[{ k: 'essentials', l: 'Essentials' }, { k: 'full', l: 'Cost breakdown' }].map(o => (
              <button key={o.k} onClick={() => setDensity(o.k)}
                className={`px-2.5 py-1 font-semibold ${density === o.k ? 'bg-[var(--leon-brown)] text-white' : 'text-[var(--leon-black)]/55 hover:bg-[var(--leon-cream)]'}`}>
                {o.l}
              </button>
            ))}
          </div>
          <span className="text-[var(--leon-black)]/40">
            {density === 'essentials'
              ? 'Overhead, freight and duty are inside Cost \u2014 open a line to see them.'
              : 'Every cost element as its own column.'}
          </span>
        </div>
      )}

      {(sub === 'supply' || sub === 'labor') && shown(sub).map(sec => (
        <QuoteSectionCard key={sec.id} ctx={ctx} project={project} qa={qa} sec={sec} editable={canEdit} density={density} />
      ))}
      {(sub === 'supply' || sub === 'labor') && !shown(sub).length && (
        <EmptyState text={`No ${sub === 'supply' ? 'supply' : 'labor'} scopes in this draft yet.`} />
      )}

      {canEdit && (sub === 'supply' || sub === 'labor') && (
        <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-3">
          {addingScope ? (
            <div className="flex items-end gap-2 flex-wrap">
              <Field label="Scope" className="flex-1 min-w-[200px]">
                {/* The live scope library — what the company actually sells —
                    rather than a fixed list that drifts from it. */}
                <Select value={newScope} onChange={e => setNewScope(e.target.value)}>
                  <option value="">Select a scope…</option>
                  {(ctx.scopeLibrary || []).filter(f => f.active !== false).map(f => (
                    <option key={f.id || f.name} value={f.name}>{f.name}</option>
                  ))}
                </Select>
              </Field>
              {/* Asked, not inferred from whichever tab you happened to be on:
                  supply and labour are separate contracts, and adding one when
                  you meant both is a scope quietly missing from the quote. */}
              <Field label="Sold as" className="min-w-[190px]">
                <Select value={newScopeKind} onChange={e => setNewScopeKind(e.target.value)}>
                  {quoteScopeIsCombined(newScope)
                    ? <option value="combined">Supply &amp; Install</option>
                    : <>
                        <option value="both">Supply + Labor (two scopes)</option>
                        <option value="supply">Supply only</option>
                        <option value="labor">Labor only</option>
                      </>}
                </Select>
              </Field>
              <Button size="sm" disabled={!newScope} onClick={() => {
                const k = quoteScopeIsCombined(newScope) ? 'combined' : newScopeKind;
                if (k === 'both') {
                  ctx.addQuoteSection(project.id, qa.id, newScope, 'supply');
                  ctx.addQuoteSection(project.id, qa.id, newScope, 'labor');
                } else {
                  ctx.addQuoteSection(project.id, qa.id, newScope, k);
                }
                setNewScope(''); setNewScopeKind('both'); setAddingScope(false);
              }}>Add</Button>
              <Button size="sm" variant="ghost" onClick={() => setAddingScope(false)}>Cancel</Button>
            </div>
          ) : (
            <button onClick={() => setAddingScope(true)} className="text-sm font-semibold text-[var(--leon-brown)]">+ Add a scope</button>
          )}
        </div>
      )}

      <div className="grid gap-3 md:grid-cols-2">
        <Field label="Assumptions" hint="What this price depends on.">
          <TextArea rows={4} value={qa.assumptions} disabled={!canEdit} onChange={e => set({ assumptions: e.target.value })} />
        </Field>
        <Field label="Exclusions" hint="What is not in it.">
          <TextArea rows={4} value={qa.exclusions} disabled={!canEdit} onChange={e => set({ exclusions: e.target.value })} />
        </Field>
      </div>

      <div className="rounded-lg bg-[var(--leon-cream)] border border-[var(--leon-line)] p-3 text-xs text-[var(--leon-black)]/60">
        This draft does not create scopes, stages or a schedule, and does not touch the contract
        value. It is a bid. When one is accepted, choosing which parts become contracted scopes is a
        separate step.
      </div>
      <ConvertQuoteModal open={converting} onClose={() => setConverting(false)}
        ctx={ctx} project={project} qa={qa} onDone={onBack} />
    </div>
  );
}

// One scope. The pricing decision is made HERE — cost + margin, or a set rate
// per unit — and any single line below can differ. Both are real: a casework
// package is priced off cost and a target margin; flooring is often just a
// known $/sq ft, and the margin is whatever it earns.
// The whole draft on one page: what each scope costs and sells, split the way
// the job is actually contracted. Supply and labour are shown apart because
// they are apart — two contracts, two invoices — and then totalled.
// ── The client's half of the quote ─────────────────────────────────────────
// The analysis holds cost, overhead, freight, duty, commission, bonus, margin
// and vendor. NONE of it may reach a client. This screen shows exactly what
// would go out, and runs clientQuoteLeaks() over the built document before
// letting anything leave — a whitelist that is checked rather than trusted.
// A quotation as slides. It takes the ALREADY-BUILT client document rather than
// the analysis, so the deck can only ever contain what the whitelist let out —
// the same document the preview on this screen is rendering.
// The references a quotation shows, in order of authority: a list curated
// under Quote Settings, else the job's own Finished Projects, else LEON's
// standard list from the shipped artwork. A quotation with no references is
// worse than one carrying the standard set.
function quoteDeckReferences(ctx) {
  const curated = (ctx.quoteArt && ctx.quoteArt.references) || null;
  if (curated && curated.length) {
    return curated.map(r => ({ id: r.id, name: r.location || r.name || '', location: '', img: r.img }));
  }
  const published = (ctx.allProjects || ctx.projects || [])
    .filter(p => p.closeout && p.closeout.portfolio && p.closeout.portfolio.published)
    .slice(0, 27)
    .map(p => {
      const pf = p.closeout.portfolio;
      const cover = (p.closeout.photos || []).find(x => x.id === pf.coverPhotoId) || (p.closeout.photos || [])[0];
      return { id: p.id, name: pf.hideClient ? (pf.title || 'Private client') : (pf.title || p.name),
               location: p.address || '', img: (cover && cover.url) || p.imageUrl || '' };
    });
  if (published.length) return published;
  return (typeof QUOTE_ART_REFERENCES !== 'undefined')
    ? QUOTE_ART_REFERENCES.map(r => ({ id: r.id, name: r.location, location: '', img: r.img })) : [];
}

function clientQuoteToSlides(ctx, project, qa, doc) {
  if (typeof officeSlidesQuoteDeck !== 'function' || typeof makeOfficeDocument !== 'function') {
    alert('LEON Presentation is not loaded.');
    return null;
  }
  const account = (ctx.accounts || []).find(a => a.id === project.accountId);
  const body = makeSlidesBody();
  body.master = quoteDeckMaster(doc, { projectName: project.name });
  // The colour boards generate themselves from the supplier catalog rather than
  // being 60 pasted photographs. Only ranges LEON sells under its OWN code can
  // appear — quoteDeckSafeSwatch drops the rest rather than publish a
  // supplier's product code on a client board.
  const colourOptions = {};
  (doc.sections || []).forEach(sec => {
    const key = typeof quoteScopeSpecKey === 'function'
      ? quoteScopeSpecKey(sec.scopeKey || sec.name) : String(sec.scopeKey || sec.name).toLowerCase();
    if (colourOptions[key]) return;
    const sup = QUOTE_DECK_COLOUR_SUPPLIERS[key];
    if (!sup || typeof searchSupplierFinishes !== 'function') return;
    const hits = searchSupplierFinishes(sup, null, '', 400).filter(r => r.leonCode);
    if (hits.length) colourOptions[key] = hits;
  });
  const references = quoteDeckReferences(ctx);
  body.slides = officeSlidesQuoteDeck(doc, {
    projectName: project.name,
    clientName: (account && account.name) || '',
    company: ctx.companyProfile || null,
    references,
    artOverrides: (ctx.quoteArt && ctx.quoteArt.overrides) || null,
    extraSpecs: qa.extraSpecs || {},
    // The analysis is passed for its LEAD TIMES only — the slides themselves
    // are built from `doc`, so nothing outside the whitelist can reach a slide.
    analysis: qa,
    terms: quoteTermsCurrent(ctx.quoteTerms) || makeQuoteTermsSet(),
    pictures: (qa.deckPictures || {}),
    structure: qa.deckSlides || null,
    colourOptions,
  });
  const d = makeOfficeDocument({
    name: `${project.name} — ${qa.name} Rev ${qa.revision}`,
    app: 'slides', folder: 'Project', projectId: project.id, body,
  }, ctx.currentUserName);
  d.activity = [{ id: uid('act'), date: todayISO(), by: ctx.currentUserName,
    text: 'Created from the client quotation.' }];
  ctx.setOfficeDocs(prev => [d].concat(prev || []));
  return d;
}


// ─── The quotation deck's pictures ───────────────────────────────────────────
// The half a generator cannot invent. They are stored on the QUOTATION
// (`qa.deckPictures`, keyed by slot) rather than on the generated slides, so
// regenerating the deck after a price change keeps every picture already
// placed — otherwise every re-issue would mean re-picking sixty images.
//
// Four sources, because that is where Leon's pictures actually live: the Render
// Library (233 items, already cut and named), a supplier finish, this job's own
// photo, or an upload.
const QUOTE_DECK_UPLOAD_MAX_PX = 1400;

// The deck's slide list, editable. Every job is different — one needs a
// per-building breakdown, another a product page, a third neither — so the
// generated sequence is a starting point rather than a fixed output.
//
// What is stored is STRUCTURE ONLY (which slide, which archetype, which scope,
// in what order). Prices, specifications and swatches are resolved from the
// live quotation when the deck is built, so an edited deck still reprices
// itself and cannot drift from what was sold.
// ─── the quotation, seen as the client will see it ───────────────────────────
// Read-only on purpose. The deck is opened in LEON Presentation's own PRESENT
// mode — the same viewer the team already uses, so there is one way a deck is
// looked at rather than a second half-built one — and nothing on screen can be
// edited from here. Editing happens in LEON Studio, deliberately a different
// door.
function QuoteDeckViewButton({ ctx, project, qa, doc, leaks }) {
  const [open, setOpen] = useState(false);
  const [body, setBody] = useState(null);
  const blocked = !!(leaks && leaks.length);

  function view() {
    if (blocked) return;
    const account = (ctx.accounts || []).find(a => a.id === project.accountId);
    const references = quoteDeckReferences(ctx);
    const colourOptions = {};
    (doc.sections || []).forEach(sec => {
      const key = quoteScopeSpecKey(sec.scopeKey || sec.name);
      if (colourOptions[key]) return;
      const sup = QUOTE_DECK_COLOUR_SUPPLIERS[key];
      if (!sup) return;
      const hits = searchSupplierFinishes(sup, null, '', 400).filter(r => r.leonCode);
      if (hits.length) colourOptions[key] = hits;
    });
    const b = makeSlidesBody();
    // WHITE pages and the master-drawn footer. Without a master the deck had no
    // background at all and every page rendered black — the presenter's own
    // backdrop showing through a slide that never painted one.
    b.master = quoteDeckMaster(doc, { projectName: project.name });
    b.slides = officeSlidesQuoteDeck(doc, {
      projectName: project.name, clientName: (account && account.name) || '',
      analysis: qa, company: ctx.companyProfile, references,
      artOverrides: (ctx.quoteArt && ctx.quoteArt.overrides) || null,
      terms: quoteTermsCurrent(ctx.quoteTerms) || makeQuoteTermsSet(),
      pictures: qa.deckPictures || {}, structure: qa.deckSlides || null,
      extraSpecs: qa.extraSpecs || {}, colourOptions,
    });
    setBody(b);
    setOpen(true);
  }

  // Three layers, and they are different things — the THEME is twelve colours
  // and two fonts, the MASTER says which of them the deck uses and what
  // furniture it carries, and T is the palette the canvas draws with, derived
  // from both. Passing the raw body as all three is what made every page black:
  // `T` was the string 'leon-corporate', so T.bg was undefined.
  const resolved = useMemo(() => {
    if (!body) return null;
    const rb = officeSlidesBody({ body });
    const theme = officeThemeOf(rb);
    return { body: rb, theme, master: rb.master, T: officeSlidesPalette(theme, rb.master) };
  }, [body]);

  return (
    <>
      <Button size="sm" variant="outline" disabled={blocked} onClick={view}
        title={blocked ? 'Held back until the leak check passes' : 'Open the quotation as the client sees it'}>
        📄 View as PDF
      </Button>
      {open && resolved && typeof OfficeSlidesPresent === 'function' ? (
        <OfficeSlidesPresent body={resolved.body} T={resolved.T} theme={resolved.theme}
          master={resolved.master} startIndex={0} onExit={() => setOpen(false)} />
      ) : null}
      {open && typeof OfficeSlidesPresent !== 'function' ? (
        <Modal open onClose={() => setOpen(false)} title="LEON Presentation is not loaded">
          <p className="text-sm">The deck viewer lives in LEON Presentation, which has not loaded. Reload the page and try again.</p>
        </Modal>
      ) : null}
    </>
  );
}

// ─── sending an issued quotation to the client ───────────────────────────────
// Offered ONLY once the quotation is Issued. A draft that has been emailed to a
// client is the one genuinely dangerous state in this module — they hold a
// version that exists nowhere — and the whole issue-before-send gate exists to
// stop it, so this button must not quietly reopen that door.
//
// The Hub cannot send mail, and this does not pretend to: it composes the
// branded letter, FILES it as the record, and hands the plain-text version to
// whatever mail client the person already uses, which genuinely sends. The
// person presses send.
// Save the quotation as a PDF.
//
// NOT through the app's own PDF writer, deliberately. That writer turns a
// region into real text and tables — which is the right answer for a report and
// the wrong one for a quotation, because it would drop every photograph and
// every layout decision the deck is made of.
//
// So the browser writes it. The slides are rendered as they actually are, one
// to a landscape sheet, and the print dialog's own "Save as PDF" produces the
// file — pictures, colour, position and all. A browser genuinely does this
// well, and the panel says plainly which button in the dialog to press.
function QuoteDeckPdfButton({ ctx, project, qa, doc, leaks }) {
  const [open, setOpen] = useState(false);
  const [body, setBody] = useState(null);
  const holder = useRef(null);
  const blocked = !!(leaks && leaks.length);

  function build() {
    if (blocked) return;
    const account = (ctx.accounts || []).find(a => a.id === project.accountId);
    const colourOptions = {};
    (doc.sections || []).forEach(sec => {
      const key = quoteScopeSpecKey(sec.scopeKey || sec.name);
      if (colourOptions[key]) return;
      const sup = QUOTE_DECK_COLOUR_SUPPLIERS[key];
      if (!sup) return;
      const hits = searchSupplierFinishes(sup, null, '', 400).filter(r => r.leonCode);
      if (hits.length) colourOptions[key] = hits;
    });
    const b = makeSlidesBody();
    b.master = quoteDeckMaster(doc, { projectName: project.name });
    b.slides = officeSlidesQuoteDeck(doc, {
      projectName: project.name, clientName: (account && account.name) || '',
      analysis: qa, company: ctx.companyProfile, references: quoteDeckReferences(ctx),
      artOverrides: (ctx.quoteArt && ctx.quoteArt.overrides) || null,
      terms: quoteTermsCurrent(ctx.quoteTerms) || makeQuoteTermsSet(),
      pictures: qa.deckPictures || {}, structure: qa.deckSlides || null,
      extraSpecs: qa.extraSpecs || {}, colourOptions,
    });
    setBody(b);
    setOpen(true);
  }

  const resolved = useMemo(() => {
    if (!body) return null;
    const rb = officeSlidesBody({ body });
    const theme = officeThemeOf(rb);
    return { body: rb, theme, master: rb.master, T: officeSlidesPalette(theme, rb.master) };
  }, [body]);

  function go() {
    // The class scopes the print to the holder; it is removed again after the
    // dialog closes so the app's own print paths are untouched.
    document.body.classList.add('qd-print-scope');
    const done = () => {
      document.body.classList.remove('qd-print-scope');
      window.removeEventListener('afterprint', done);
    };
    window.addEventListener('afterprint', done);
    // A frame, so the holder is laid out before the dialog freezes the page.
    requestAnimationFrame(() => { window.print(); setTimeout(done, 1500); });
  }

  const slides = resolved ? (resolved.body.slides || []).filter(s => !s.hidden) : [];
  const numbers = resolved ? officeSlidesNumbers(resolved.body.slides || []) : { map: {} };

  return (
    <>
      <Button size="sm" variant="outline" disabled={blocked} onClick={build}
        title={blocked ? 'Held back until the leak check passes' : 'Write the quotation out as a PDF'}>
        ⬇ Save as PDF
      </Button>
      {open && resolved ? (
        <Modal open onClose={() => setOpen(false)} size="lg" title="Save this quotation as a PDF"
          footer={<>
            <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
            <Button onClick={go}>Open the print dialog</Button>
          </>}>
          <div className="space-y-3 text-sm">
            <p><b>{slides.length} pages</b>, exactly as the deck reads — pictures, colour and all.</p>
            <div className="rounded-md bg-[var(--leon-cream)] p-3 text-xs space-y-1">
              <div className="font-bold">In the dialog that opens:</div>
              <div>1. Destination — <b>Save as PDF</b></div>
              <div>2. Layout — <b>Landscape</b></div>
              <div>3. Margins — <b>None</b>, and switch <b>Background graphics</b> ON</div>
              <div className="pt-1 text-[var(--leon-black)]/60">
                Without background graphics the photographs and the tinted bands are dropped —
                browsers leave them off by default to save ink.
              </div>
            </div>
            <p className="text-xs text-[var(--leon-black)]/60">
              The browser writes this file, not the Hub. That is deliberate: the app's own PDF
              writer produces real searchable text and would drop every photograph, which is the
              wrong trade for a quotation.
            </p>
          </div>
        </Modal>
      ) : null}
      {/* Off-screen until the print rules bring it forward. */}
      {resolved ? (
        <div id="qd-print-holder" ref={holder} aria-hidden="true">
          {slides.map(s => (
            <div key={s.id} className="qd-sheet" style={{ width: '100%' }}>
              <OfficeSlidesCanvas slide={s} T={resolved.T} theme={resolved.theme}
                master={resolved.master} number={numbers.map[s.id]} ratio={resolved.body.size}
                editable={false} selectedIds={[]} onSelect={() => {}} onApply={() => {}} present />
            </div>
          ))}
        </div>
      ) : null}
    </>
  );
}

function QuoteEmailClientButton({ ctx, project, qa, doc }) {
  const [open, setOpen] = useState(false);
  const [to, setTo] = useState('');
  const [msg, setMsg] = useState('');
  const issued = qa.status === 'Issued' || qa.status === 'Superseded';
  if (!issued) return null;

  const account = (ctx.accounts || []).find(a => a.id === project.accountId);
  const contacts = (account && account.contacts || []).filter(c => c.email);
  const money = n => '$' + Math.round(qnum(n)).toLocaleString();
  const subject = `${project.name} — Interior Finishes Estimate — Rev ${String(qa.revision || 1).padStart(2, '0')}`;
  const bodyText = [
    `Please find our interior finishes estimate for ${project.name}.`,
    '',
    `Quotation: ${qa.name} — Rev ${String(qa.revision || 1).padStart(2, '0')}`,
    `Issued: ${fmtDate(qa.issuedDate) || fmtDate(todayISO())}`,
    `Contract value: ${money(doc.contractValue)}`,
    '',
    msg || '',
    '',
    'The full estimate is attached.',
  ].filter(x => x !== undefined).join('\n');

  function send() {
    if (typeof ctx.fileOutgoingEmail === 'function') {
      ctx.fileOutgoingEmail({ to, toName: '', subject, body: bodyText, html: null, projectId: project.id });
    }
    // Cc the sender: a quotation to a client is correspondence they are
    // responsible for, and the copy belongs in their own mailbox. That is the
    // same rule every share email in the Hub follows.
    const url = buildMailtoUrl({ to, cc: (ctx.currentUser && ctx.currentUser.email) || '', subject, body: bodyText });
    window.location.href = url;
    setOpen(false);
  }

  return (
    <>
      <Button size="sm" variant="outline" onClick={() => setOpen(true)}>✉️ Email to client</Button>
      <Modal open={open} onClose={() => setOpen(false)} title={'Email Rev ' + String(qa.revision || 1).padStart(2, '0') + ' to the client'}
        footer={<>
          <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
          <Button disabled={!to} onClick={send}>Open in my mail app</Button>
        </>}>
        <div className="space-y-3">
          <Field label="To">
            <div className="flex gap-2">
              <Select className="w-56" value="" onChange={e => { if (e.target.value) setTo(e.target.value); }}>
                <option value="">Pick a contact…</option>
                {contacts.map(c => <option key={c.id} value={c.email}>{c.name} — {c.email}</option>)}
              </Select>
              <TextInput className="flex-1" value={to} onChange={e => setTo(e.target.value)} placeholder="name@company.com" />
            </div>
          </Field>
          <Field label="Anything to add"><TextArea rows={4} value={msg} onChange={e => setMsg(e.target.value)} /></Field>
          <div className="rounded-md bg-[var(--leon-cream)] p-2 text-xs whitespace-pre-wrap">{subject}{'\n\n'}{bodyText}</div>
          <p className="text-xs text-[var(--leon-black)]/60">
            The Hub has no mail server and cannot have one, so this hands the message to your own mail
            app — it sends from your address and lands in your Sent folder, and <b>you press send</b>.
            A copy is filed against the job either way. <b>Attach the PDF yourself</b>: a link cannot
            carry a file. Use <b>View as PDF</b> and print it to a file first.
          </p>
        </div>
      </Modal>
    </>
  );
}

// A hand-inserted slide's own content. Some pages belong on the quotation the
// client reads and nowhere near the analysis — an Accessories page listing
// cutlery inserts and waste bins is the case that prompted this: the analysis
// prices casework per module, so those would be a column that is always blank.
// So this content lives on the SLIDE, is presentation-only, and never reaches
// the line list, the totals or the client-quote whitelist.
function QuoteDeckManualSlideModal({ row, onSave, onClose }) {
  const isProduct = row.archetype === 'product';
  const [title, setTitle] = useState(row.specTitle || row.title || '');
  const [rows, setRows] = useState(() => isProduct
    ? (row.lines || []).map(l => ({ value: l }))
    : (row.specs || []).map(r => ({ field: r.field || '', value: r.value || '' })));

  function set(i, patch) { setRows(rs => rs.map((r, j) => j === i ? Object.assign({}, r, patch) : r)); }
  function save() {
    const clean = rows.filter(r => (r.field || '').trim() || (r.value || '').trim());
    onSave(isProduct
      ? { specTitle: title, lines: clean.map(r => r.value) }
      : { specTitle: title, specs: clean.map(r => ({ field: r.field || '', value: r.value || '' })) });
  }

  return (
    <Modal open onClose={onClose} size="lg"
      title={'Edit this slide — ' + ((quoteDeckArchetype(row.archetype) || {}).label || row.archetype)}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={save}>Save</Button>
      </>}>
      <div className="space-y-3">
        <Field label="Heading">
          <TextInput value={title} onChange={e => setTitle(e.target.value)}
            placeholder={isProduct ? 'e.g. SPC Flooring' : 'e.g. Accessories'} />
        </Field>
        <div className="space-y-1">
          {rows.map((r, i) => (
            <div key={i} className="flex gap-2">
              {!isProduct ? (
                <TextInput className="w-52" placeholder="Field (e.g. Cutlery)"
                  value={r.field || ''} onChange={e => set(i, { field: e.target.value })} />
              ) : null}
              <TextInput className="flex-1" placeholder={isProduct ? 'A line of specification' : 'Value (e.g. Blum Orga-Line)'}
                value={r.value || ''} onChange={e => set(i, { value: e.target.value })} />
              <Button variant="danger" size="sm" onClick={() => setRows(rs => rs.filter((_, j) => j !== i))}>×</Button>
            </div>
          ))}
          <Button variant="ghost" size="sm"
            onClick={() => setRows(rs => rs.concat([isProduct ? { value: '' } : { field: '', value: '' }]))}>+ Add a line</Button>
        </div>
        <p className="text-xs text-[var(--leon-black)]/60">
          This page is part of the <b>quotation</b> only. It is not a line on the quote analysis, it
          carries no price, and it changes no total — which is exactly why it can say things the
          analysis has no column for.
        </p>
      </div>
    </Modal>
  );
}

function QuoteDeckSlidesPanel({ ctx, project, qa, doc, editable }) {
  const [insertAt, setInsertAt] = useState(null);
  const [editRow, setEditRow] = useState(null);
  const opts = useMemo(() => ({ projectName: project.name, analysis: qa,
    company: ctx.companyProfile, terms: quoteTermsCurrent(ctx.quoteTerms) }), [project.name, qa, ctx.companyProfile, ctx.quoteTerms]);
  const res = useMemo(() => {
    try { return quoteDeckResolve(doc, opts, qa.deckSlides); } catch (e) { return null; }
  }, [doc, opts, qa.deckSlides]);
  if (!res) return null;
  const structure = (qa.deckSlides && qa.deckSlides.length) ? qa.deckSlides : quoteDeckStructure(res.plan);
  const saved = !!(qa.deckSlides && qa.deckSlides.length);

  function write(rows) { ctx.updateQuoteAnalysis(project.id, qa.id, { deckSlides: rows }); }
  function move(i, dir) {
    const j = i + dir; if (j < 0 || j >= structure.length) return;
    const n = structure.slice(); const t = n[i]; n[i] = n[j]; n[j] = t; write(n);
  }
  function remove(i) { write(structure.filter((_, j) => j !== i)); }
  function insert(archetype, scope) {
    const a = quoteDeckArchetype(archetype);
    const row = { id: uid('dslide'), archetype, scope: scope || null,
      title: scope ? String(scope).replace(/\s+[—-]\s+(Supply|Labor).*$/i, '') : (a ? a.label : ''),
      manual: true };
    const n = structure.slice(); n.splice(insertAt + 1, 0, row); write(n);
    setInsertAt(null);
  }
  function rebuild() {
    if (saved && !window.confirm('Rebuild the slide list from the quotation? Slides you added by hand, and any you removed, will be lost.')) return;
    ctx.updateQuoteAnalysis(project.id, qa.id, { deckSlides: null });
  }

  return (
    <Collapsible id={'qdeck-slides-' + qa.id} title="Quotation deck — slides"
      count={structure.length}
      right={saved ? <Badge tone="brown">arranged</Badge> : <Badge tone="neutral">as generated</Badge>}
      defaultOpen={false}>
      <div className="space-y-2">
        <p className="text-xs text-[var(--leon-black)]/60">
          Add, remove or reorder slides — the format stays the same. Only the arrangement is kept;
          prices and specifications are read from the quotation each time the deck is built, so an
          arranged deck still reprices itself.
        </p>
        {res.orphans.length ? (
          <div className="rounded-md bg-[var(--leon-red)]/10 border border-[var(--leon-red)]/30 p-2 text-xs">
            <b>{res.orphans.length} slide{res.orphans.length === 1 ? '' : 's'} point at a scope that has left this
            quotation</b> — {res.orphans.map(o => o.scope).join(', ')}. They are skipped when the deck is built.
            {editable ? <> <button className="underline font-semibold ml-1"
              onClick={() => write(structure.filter(r => !res.orphans.some(o => o.id === r.id)))}>Remove them</button></> : null}
          </div>
        ) : null}
        {res.added.length ? (
          <div className="rounded-md bg-[var(--leon-yellow)]/20 border border-[var(--leon-yellow)]/40 p-2 text-xs">
            <b>{res.added.length} slide{res.added.length === 1 ? '' : 's'} for scopes added since this deck was
            arranged</b> are not in it.
            {editable ? <> <button className="underline font-semibold ml-1"
              onClick={() => write(structure.concat(quoteDeckStructure(res.added)))}>Add them at the end</button></> : null}
          </div>
        ) : null}

        <div className="rounded-lg border border-[var(--leon-line)] bg-white divide-y divide-[var(--leon-line)]">
          {structure.map((row, i) => {
            const a = quoteDeckArchetype(row.archetype);
            const orphan = res.orphans.some(o => o.id === row.id);
            return (
              <div key={row.id || i} className={'flex items-center gap-2 px-2 py-1.5 text-xs ' + (orphan ? 'bg-[var(--leon-red)]/5' : '')}>
                <span className="w-6 text-[var(--leon-black)]/40 tabular-nums">{i + 1}</span>
                <span className="font-semibold w-40">{a ? a.label : row.archetype}</span>
                <span className="flex-1 text-[var(--leon-black)]/60 truncate">
                  {row.specTitle ? <b>{row.specTitle}</b> : null}
                  {row.specTitle && row.scope ? ' · ' : ''}
                  {row.scope || (row.specTitle ? '' : row.title || '')}
                  {row.manual && (row.archetype === 'spec' || row.archetype === 'product') && !((row.specs || []).length || (row.lines || []).length)
                    ? <span className="text-[var(--leon-brown)]"> — empty, press ✎</span> : null}
                </span>
                {row.manual ? <Badge tone="neutral">added</Badge> : null}
                {orphan ? <Badge tone="red">scope gone</Badge> : null}
                {a && a.pictures ? <span className="text-[var(--leon-black)]/40">{a.pictures} pic</span> : null}
                {editable && row.manual && (row.archetype === 'spec' || row.archetype === 'product')
                  ? <Button variant="ghost" size="sm" onClick={() => setEditRow({ row, i })} title="Edit this slide's content">✎</Button>
                  : null}
                {editable ? <>
                  <Button variant="ghost" size="sm" onClick={() => move(i, -1)} title="Move up">↑</Button>
                  <Button variant="ghost" size="sm" onClick={() => move(i, 1)} title="Move down">↓</Button>
                  <Button variant="ghost" size="sm" onClick={() => setInsertAt(i)} title="Insert a slide after this one">+</Button>
                  <Button variant="danger" size="sm" onClick={() => remove(i)} title="Remove this slide">×</Button>
                </> : null}
              </div>
            );
          })}
        </div>
        {editable ? (
          <div className="flex gap-2">
            <Button variant="ghost" size="sm" onClick={() => setInsertAt(structure.length - 1)}>+ Add a slide at the end</Button>
            <Button variant="ghost" size="sm" onClick={rebuild}>Rebuild from the quotation</Button>
          </div>
        ) : null}
      </div>
      {editRow ? (
        <QuoteDeckManualSlideModal row={editRow.row}
          onClose={() => setEditRow(null)}
          onSave={patch => {
            const n = structure.slice();
            n[editRow.i] = Object.assign({}, n[editRow.i], patch, { title: patch.specTitle || n[editRow.i].title });
            write(n); setEditRow(null);
          }} />
      ) : null}
      {insertAt != null ? (
        <Modal open onClose={() => setInsertAt(null)} title={'Insert a slide after #' + (insertAt + 1)}>
          <div className="space-y-2">
            <p className="text-xs text-[var(--leon-black)]/60">
              The cover, estimate, summary and terms slides are not offered — those are the quotation
              stating itself, and a deck with two bid summaries is not a variation.
            </p>
            {QUOTE_DECK_INSERTABLE.map(k => {
              const a = quoteDeckArchetype(k);
              if (!a) return null;
              return (
                <div key={k} className="flex items-center gap-2">
                  <span className="w-40 text-sm font-semibold">{a.label}</span>
                  {a.perScope ? (
                    <Select className="flex-1" defaultValue=""
                      onChange={e => { if (e.target.value) insert(k, e.target.value); }}>
                      <option value="">Choose a scope…</option>
                      {(doc.sections || []).map(s => <option key={s.name} value={s.name}>{s.name}</option>)}
                    </Select>
                  ) : <Button variant="outline" size="sm" onClick={() => insert(k, null)}>Insert</Button>}
                </div>
              );
            })}
          </div>
        </Modal>
      ) : null}
    </Collapsible>
  );
}

function QuoteDeckPicturesPanel({ ctx, project, qa, doc, editable }) {
  const [picking, setPicking] = useState(null);
  const [wizard, setWizard] = useState(false);
  const pics = qa.deckPictures || {};
  const plan = useMemo(() => {
    try { return quoteDeckPlan(doc, { projectName: project.name }); } catch (e) { return []; }
  }, [doc, project.name]);
  const steps = plan.map((s, i) => ({ s, i })).filter(x => (x.s.pictures || 0) > 0);
  const missing = quoteDeckMissingPictures(plan, pics);
  const total = plan.reduce((n, s) => n + (s.pictures || 0), 0);

  function setSlot(slot, value) {
    const next = Object.assign({}, qa.deckPictures || {});
    if (value) next[slot] = value; else delete next[slot];
    ctx.updateQuoteAnalysis(project.id, qa.id, { deckPictures: next });
  }

  return (
    <Collapsible id={'qdeck-pics-' + qa.id} title="Quotation deck — pictures"
      count={total - missing} defaultOpen={false}
      right={missing
        ? <Badge tone="yellow">{missing} still to place</Badge>
        : <Badge tone="green">all placed</Badge>}>
      <div className="space-y-3">
        {editable ? (
          <Button variant="primary" size="sm" onClick={() => setWizard(true)}>
            Go through them one by one →
          </Button>
        ) : null}
        <p className="text-xs text-[var(--leon-black)]/60">
          These are kept on the quotation, not on the slides, so re-generating the deck after a
          price change keeps every picture already placed. An empty slot prints a visible
          placeholder rather than a blank rectangle — a deck should not reach a client with
          holes in it.
        </p>
        {steps.map(({ s, i }) => (
          <div key={i} className="rounded-lg border border-[var(--leon-line)] bg-white p-2">
            <div className="text-xs font-bold mb-2">
              <span className="text-[var(--leon-black)]/40 mr-2">{i + 1}.</span>
              {s.label}{s.title ? ' — ' + s.title : ''}
            </div>
            <div className="flex flex-wrap gap-2">
              {Array.from({ length: quoteDeckSlotsFor(s, i, pics) }).map((_, k) => {
                const slot = s.archetype + ':' + i + ':' + k;
                const got = pics[slot];
                return (
                  <div key={k} className="relative">
                    <button type="button"
                      onClick={() => editable && setPicking({ slot, label: s.label + (s.title ? ' — ' + s.title : '') })}
                      className={'w-24 h-20 rounded-md border overflow-hidden flex items-center justify-center text-[10px] '
                        + (got ? 'border-[var(--leon-line)]' : 'border-dashed border-[var(--leon-brown)]/50 text-[var(--leon-brown)]')
                        + (editable ? ' hover:opacity-80 cursor-pointer' : ' cursor-default')}
                      title={got ? (got.name || 'Replace this picture') : 'Add a picture'}>
                      {got
                        ? <img src={got.url} alt={got.name || ''} className="w-full h-full object-cover" />
                        : <span>{k < (s.pictures || 0) ? '+ Add picture' : '+ Add another'}</span>}
                    </button>
                    {got && editable ? (
                      <button type="button" onClick={() => setSlot(slot, null)} title="Clear this slot"
                        className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-[var(--leon-red)] text-white text-[10px] leading-none">×</button>
                    ) : null}
                  </div>
                );
              })}
            </div>
          </div>
        ))}
        {!steps.length ? <div className="text-sm text-[var(--leon-black)]/60">No picture slots — this quotation has no priced scopes yet.</div> : null}
      </div>
      {picking ? (
        <QuoteDeckPicturePicker ctx={ctx} project={project} label={picking.label}
          onClose={() => setPicking(null)}
          onPick={v => { setSlot(picking.slot, v); setPicking(null); }} />
      ) : null}
      {wizard ? (
        <QuoteDeckPictureWizard ctx={ctx} project={project} qa={qa} doc={doc}
          structure={qa.deckSlides}
          onClose={() => setWizard(false)}
          onWrite={(rows, nextPics) => ctx.updateQuoteAnalysis(project.id, qa.id,
            { deckSlides: rows, deckPictures: nextPics })} />
      ) : null}
    </Collapsible>
  );
}

// The lead times an estimate slide states, and the extra specification lines
// the wizard deliberately does not ask for.
//
// Both live on the QUOTATION rather than in the wizard, which is the whole
// point: the wizard asks the questions every scope needs and would be
// unusable if it asked every question any scope might need. This is where the
// rest goes, once, on the quotation it applies to.
function QuoteDeckDetailsPanel({ ctx, project, qa, doc, editable }) {
  const sections = (doc.sections || []);
  const job = qa.leadTimes || {};
  const extras = qa.extraSpecs || {};

  function setJobLead(key, v) {
    const next = Object.assign({}, qa.leadTimes || {});
    if (v === '' || v == null) delete next[key]; else next[key] = qnum(v);
    ctx.updateQuoteAnalysis(project.id, qa.id, { leadTimes: Object.keys(next).length ? next : null });
  }
  function setExtras(secName, rows) {
    const next = Object.assign({}, qa.extraSpecs || {});
    if (rows && rows.length) next[secName] = rows; else delete next[secName];
    ctx.updateQuoteAnalysis(project.id, qa.id, { extraSpecs: next });
  }

  return (
    <Collapsible id={'qdeck-details-' + qa.id} title="Quotation deck — lead times & extra lines" defaultOpen={false}>
      <div className="space-y-4">
        <div>
          <div className="text-xs font-bold mb-1">Lead times stated on every estimate slide</div>
          <p className="text-xs text-[var(--leon-black)]/60 mb-2">
            Blank follows LEON's standard ({QUOTE_DECK_LEADTIME_FIELDS.map(f =>
              f.label.toLowerCase() + ' ' + QUOTE_DECK_LEADTIME_DEFAULT[f.key]).join(', ')} weeks).
            A number here applies to this quotation.
          </p>
          <div className="flex flex-wrap gap-3">
            {QUOTE_DECK_LEADTIME_FIELDS.map(f => (
              <label key={f.key} className="text-xs">
                <div className="mb-0.5 text-[var(--leon-black)]/60">{f.label}</div>
                <TextInput type="number" className="w-28" disabled={!editable}
                  value={job[f.key] == null ? '' : job[f.key]}
                  placeholder={String(QUOTE_DECK_LEADTIME_DEFAULT[f.key])}
                  onChange={e => setJobLead(f.key, e.target.value)} />
              </label>
            ))}
          </div>
        </div>

        <div>
          <div className="text-xs font-bold mb-1">Extra specification lines</div>
          <p className="text-xs text-[var(--leon-black)]/60 mb-2">
            The specification slide states whatever the quotation already answers. Anything else
            this job needs said goes here — it is added to that scope's slide and nowhere else, so
            the wizard stays as short as it is.
          </p>
          <div className="space-y-2">
            {sections.map(sec => (
              <QuoteDeckExtraSpecRows key={sec.name} sec={sec} editable={editable}
                rows={extras[sec.name] || []} onChange={rows => setExtras(sec.name, rows)} />
            ))}
          </div>
        </div>
      </div>
    </Collapsible>
  );
}

function QuoteDeckExtraSpecRows({ sec, rows, onChange, editable }) {
  function set(i, patch) {
    const n = rows.map((r, j) => j === i ? Object.assign({}, r, patch) : r);
    onChange(n);
  }
  return (
    <div className="rounded-md border border-[var(--leon-line)] bg-white p-2">
      <div className="text-xs font-bold mb-1">{sec.name}</div>
      {rows.map((r, i) => (
        <div key={i} className="flex gap-2 mb-1">
          <TextInput className="w-44" placeholder="Field" value={r.field || ''} disabled={!editable}
            onChange={e => set(i, { field: e.target.value })} />
          <TextInput className="flex-1" placeholder="Value" value={r.value || ''} disabled={!editable}
            onChange={e => set(i, { value: e.target.value })} />
          {editable ? <Button variant="danger" size="sm"
            onClick={() => onChange(rows.filter((_, j) => j !== i))}>×</Button> : null}
        </div>
      ))}
      {editable ? <Button variant="ghost" size="sm"
        onClick={() => onChange(rows.concat([{ field: '', value: '' }]))}>+ Add a line</Button> : null}
    </div>
  );
}

// The pictures, one slide at a time.
//
// The panel lists every slot at once, which is right for checking what is still
// missing and wrong for actually filling them in — seventy thumbnails is a wall,
// and the question at each one is small: this slide, this picture, or drop the
// slide. So this walks them, and the only three answers are the three buttons.
//
// It walks SLIDES rather than slots, because "delete this slide" is an answer
// to the slide and not to one of its pictures.
function QuoteDeckPictureWizard({ ctx, project, qa, doc, structure, onClose, onWrite }) {
  const res = useMemo(() => {
    try {
      return quoteDeckResolve(doc, { projectName: project.name, analysis: qa,
        company: ctx.companyProfile, terms: quoteTermsCurrent(ctx.quoteTerms) }, structure);
    } catch (e) { return null; }
  }, [doc, project.name, qa, ctx.companyProfile, ctx.quoteTerms, structure]);
  const steps = useMemo(() => (res ? res.plan
    .map((s, i) => ({ s, i }))
    .filter(x => (x.s.pictures || 0) > 0) : []), [res]);
  // Opens on the first slide that still has a gap, not on slide one. A second
  // pass is looking for what is missing, and walking past everything already
  // done to find it is the whole cost of a wizard. Falls back to the start when
  // nothing is missing, so a finished deck can still be reviewed from the top.
  const [at, setAt] = useState(() => {
    try {
      const p = quoteDeckResolve(doc, { projectName: project.name, analysis: qa,
        company: ctx.companyProfile, terms: quoteTermsCurrent(ctx.quoteTerms) }, structure).plan;
      const withPics = p.map((x, i) => ({ x, i })).filter(y => (y.x.pictures || 0) > 0);
      const held = qa.deckPictures || {};
      const gap = withPics.findIndex(y => {
        for (let k = 0; k < (y.x.pictures || 0); k++) {
          const slot = y.x.archetype + ':' + y.i + ':' + k;
          if (!held[slot] || !held[slot].url) return true;
        }
        return false;
      });
      return gap >= 0 ? gap : 0;
    } catch (e) { return 0; }
  });
  const [picking, setPicking] = useState(null);
  const pics = qa.deckPictures || {};

  if (!res) return null;
  if (!steps.length) {
    return (
      <Modal open onClose={onClose} title="Pictures" footer={<Button onClick={onClose}>Close</Button>}>
        <p className="text-sm">No slide in this deck takes a picture. Section dividers use LEON's
          standard artwork, so there is nothing to place.</p>
      </Modal>
    );
  }
  const cur = steps[Math.min(at, steps.length - 1)];
  // However many are placed, plus one empty to place the next in.
  const slotCount = quoteDeckSlotsFor(cur.s, cur.i, pics);
  const slots = Array.from({ length: slotCount }).map((_, k) => cur.s.archetype + ':' + cur.i + ':' + k);
  const filledHere = slots.filter(k => pics[k] && pics[k].url).length;
  const missingAll = quoteDeckMissingPictures(res.plan, pics);
  const totalAll = res.plan.reduce((n, s) => n + (s.pictures || 0), 0);

  function setSlot(slot, value) {
    const next = Object.assign({}, qa.deckPictures || {});
    if (value) next[slot] = value; else delete next[slot];
    ctx.updateQuoteAnalysis(project.id, qa.id, { deckPictures: next });
  }
  function setCaption(slot, caption) {
    const cur2 = (qa.deckPictures || {})[slot];
    if (!cur2) return;
    setSlot(slot, Object.assign({}, cur2, { caption }));
  }
  // Removing the LAST picture on a slide would otherwise leave a gap that the
  // ones after it never close, because slots are addressed by position. So the
  // ones after it shuffle down.
  function removeSlot(slot) {
    const k = parseInt(slot.slice(slot.lastIndexOf(':') + 1), 10);
    const prefix = cur.s.archetype + ':' + cur.i + ':';
    const next = Object.assign({}, qa.deckPictures || {});
    delete next[slot];
    for (let n = k + 1; n <= slotCount; n++) {
      if (next[prefix + n]) { next[prefix + (n - 1)] = next[prefix + n]; delete next[prefix + n]; }
    }
    ctx.updateQuoteAnalysis(project.id, qa.id, { deckPictures: next });
  }
  function dropSlide() {
    const row = structure && structure.length
      ? structure[cur.i]
      : quoteDeckStructure(res.plan)[cur.i];
    if (!row) return;
    if (!window.confirm(`Remove the ${(quoteDeckArchetype(cur.s.archetype) || {}).label || cur.s.archetype} slide${cur.s.title ? ' for ' + cur.s.title : ''} from this deck?`)) return;
    const base = (structure && structure.length) ? structure : quoteDeckStructure(res.plan);
    // Its pictures go with it — leaving them keyed to an index that no longer
    // exists is how a slot silently reappears on the wrong slide later.
    const next = Object.assign({}, qa.deckPictures || {});
    slots.forEach(k => { delete next[k]; });
    onWrite(base.filter((_, j) => j !== cur.i), next);
    setAt(a => Math.min(a, steps.length - 2 < 0 ? 0 : steps.length - 2));
  }
  const step = (d) => setAt(a => Math.max(0, Math.min(steps.length - 1, a + d)));

  return (
    <>
      <Modal open onClose={onClose} size="lg"
        title={`Pictures — slide ${at + 1} of ${steps.length}`}
        footer={<>
          <Button variant="ghost" onClick={onClose}>Done</Button>
          <div className="flex-1" />
          <Button variant="danger" onClick={dropSlide}>Remove this slide</Button>
          <Button variant="ghost" disabled={at === 0} onClick={() => step(-1)}>← Back</Button>
          <Button disabled={at >= steps.length - 1} onClick={() => step(1)}>
            {filledHere ? 'Next →' : 'Skip →'}
          </Button>
        </>}>
        <div className="space-y-3">
          <div className="flex items-center gap-2 text-xs">
            <Badge tone="neutral">{(quoteDeckArchetype(cur.s.archetype) || {}).label || cur.s.archetype}</Badge>
            {cur.s.title ? <span className="font-semibold">{cur.s.title}</span> : null}
            <div className="flex-1" />
            <span className="text-[var(--leon-black)]/55">
              {totalAll - missingAll} of {totalAll} placed across the deck
            </span>
          </div>
          <div className="h-1 rounded bg-[var(--leon-line)] overflow-hidden">
            <div className="h-full bg-[var(--leon-brown)]" style={{ width: ((at + 1) / steps.length * 100) + '%' }} />
          </div>

          <div className="flex flex-wrap gap-3">
            {slots.map((slot, k) => {
              const got = pics[slot];
              return (
                <div key={slot} className="w-56">
                  <button type="button"
                    onClick={() => setPicking({ slot, label: (cur.s.title || '') + ' — ' + ((quoteDeckArchetype(cur.s.archetype) || {}).label || '') })}
                    className={'w-56 h-40 rounded-md border overflow-hidden flex items-center justify-center text-xs '
                      + (got ? 'border-[var(--leon-line)]' : 'border-dashed border-[var(--leon-brown)]/50 text-[var(--leon-brown)] hover:bg-[var(--leon-cream)]')}>
                    {got ? <img src={got.url} alt={got.name || ''} className="w-full h-full object-cover" />
                         : <span>{k < (cur.s.pictures || 0) ? '+ Add picture' : '+ Add another'}</span>}
                  </button>
                  {got ? (
                    <div className="mt-1 space-y-1">
                      <TextInput className="!text-[11px] !py-1" value={got.caption || ''}
                        placeholder="Caption (optional)"
                        onChange={e => setCaption(slot, e.target.value)} />
                      <div className="flex gap-2 text-[11px]">
                        <button className="font-semibold text-[var(--leon-brown)]"
                          onClick={() => setPicking({ slot, label: cur.s.title || '' })}>Replace</button>
                        <button className="text-[var(--leon-red)]" onClick={() => removeSlot(slot)}>Remove</button>
                        {got.name ? <span className="ml-auto truncate text-[var(--leon-black)]/45">{got.name}</span> : null}
                      </div>
                    </div>
                  ) : null}
                </div>
              );
            })}
          </div>
          <p className="text-xs text-[var(--leon-black)]/60">
            A caption is optional: leave it blank and the slide shows the picture alone, with no
            label and no space taken for one. The last frame is always empty, so a slide can carry
            as many pictures as the job needs.
          </p>
          <p className="text-xs text-[var(--leon-black)]/60">
            Leave a slot empty and the slide prints a placeholder — the deck counts those, so
            nothing reaches a client unnoticed. Remove the slide instead if this job does not need it.
          </p>
        </div>
      </Modal>
      {picking ? (
        <QuoteDeckPicturePicker ctx={ctx} project={project} label={picking.label}
          onClose={() => setPicking(null)}
          onPick={v => { setSlot(picking.slot, v); setPicking(null); }} />
      ) : null}
    </>
  );
}

function QuoteDeckPicturePicker({ ctx, project, label, onClose, onPick }) {
  const [src, setSrc] = useState('render');
  const [q, setQ] = useState('');
  const [sup, setSup] = useState('');
  const [busy, setBusy] = useState(false);
  const [notice, setNotice] = useState(null);

  const renders = useMemo(() => {
    const all = renderItems();
    const t = q.trim().toLowerCase();
    return (t ? all.filter(i => (i.name + ' ' + (i.cat || '') + ' ' + (i.style || '') + ' ' + (i.finish || '')).toLowerCase().includes(t)) : all).slice(0, 60);
  }, [q]);
  const groups = useMemo(() => (typeof supplierGroups === 'function' ? supplierGroups() : []), []);
  const finishes = useMemo(() => {
    if (!sup) return [];
    return searchSupplierFinishes(sup, null, q, 60).filter(r => r.img);
  }, [sup, q]);
  const projectPhotos = useMemo(() => {
    const out = [];
    if (project.imageUrl) out.push({ url: project.imageUrl, name: project.name });
    ((project.closeout && project.closeout.photos) || []).forEach(p => {
      if (p.url) out.push({ url: p.url, name: p.caption || 'Finished photo' });
    });
    return out;
  }, [project]);

  // The FIRST PAGE of a drawing set already on the job. A quotation's Project
  // Plans page is the architect's plan, and it is already here — asking someone
  // to export page one and upload it again is asking them to keep a second copy
  // of a file we already hold.
  const sets = (project.drawingSets || []).filter(d => d.status !== 'Void' && d.fileUrl);
  async function pickDrawing(d) {
    if (typeof officePdfCoverImage !== 'function') { setNotice('The PDF reader has not loaded.'); return; }
    setBusy(true); setNotice(null);
    try {
      const got = await officePdfCoverImage(d.fileUrl, { width: 1600 });
      setBusy(false);
      if (!got || !got.url) {
        setNotice('That attachment is neither a PDF nor an image, so a first page cannot be taken from it.');
        return;
      }
      onPick({ source: 'drawing', id: d.id, url: got.url,
        name: d.name + (d.revision ? ' Rev ' + d.revision : '') });
    } catch (err) {
      setBusy(false);
      setNotice('Could not read that drawing set: ' + (err && err.message ? err.message : 'unknown error'));
    }
  }

  async function onUpload(e) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setBusy(true);
    const dataUrl = await new Promise(res => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(file); });
    // Downscaled before it is stored. A deck picture straight off a phone is
    // several megabytes and the whole app's state budget is about thirteen.
    const shrunk = await shrinkSignature(dataUrl, QUOTE_DECK_UPLOAD_MAX_PX);
    setBusy(false);
    onPick({ source: 'upload', url: shrunk.url, name: file.name });
  }

  const SRC = [
    { key: 'render', label: 'Render Library' },
    { key: 'finish', label: 'Supplier finish' },
    { key: 'drawing', label: 'Drawing set' },
    { key: 'project', label: 'Project photo' },
    { key: 'upload', label: 'Upload' },
  ];
  return (
    <Modal open onClose={onClose} size="lg" title={'Add a picture — ' + label}>
      <div className="space-y-3">
        <div className="flex flex-wrap gap-2">
          {SRC.map(s => (
            <button key={s.key} type="button" onClick={() => { setSrc(s.key); setQ(''); }}
              className={'subtab-btn px-3 py-1.5 rounded-md text-xs font-semibold '
                + (src === s.key ? 'bg-[var(--leon-black)] text-white' : 'bg-[var(--leon-cream)]')}>{s.label}</button>
          ))}
        </div>

        {src === 'render' ? <>
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search 233 render items…" />
          <div className="grid grid-cols-4 sm:grid-cols-6 gap-2 max-h-80 overflow-y-auto">
            {renders.map(i => (
              <button key={i.id} type="button" className="text-left"
                onClick={() => onPick({ source: 'render', id: i.id, url: i.img, name: i.name })}>
                <img src={i.thumb || i.img} alt={i.name} className="w-full h-20 object-cover rounded-md border border-[var(--leon-line)]" />
                <div className="text-[10px] mt-0.5 truncate">{i.name}</div>
              </button>
            ))}
            {!renders.length ? <div className="col-span-full text-sm text-[var(--leon-black)]/60">Nothing matches.</div> : null}
          </div>
        </> : null}

        {src === 'finish' ? <>
          <div className="flex gap-2">
            <Select value={sup} onChange={e => setSup(e.target.value)} className="w-56">
              <option value="">Choose a supplier…</option>
              {groups.map(g => <option key={g.sup} value={g.sup}>{supplierDisplayName(g.sup)}</option>)}
            </Select>
            <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search by name or code…" className="flex-1" />
          </div>
          <div className="grid grid-cols-4 sm:grid-cols-6 gap-2 max-h-80 overflow-y-auto">
            {finishes.map(f => (
              <button key={f.id} type="button" className="text-left"
                onClick={() => onPick({ source: 'finish', id: f.id, url: f.img, name: f.leonCode || f.name })}>
                <img src={f.img} alt={f.name} className="w-full h-20 object-cover rounded-md border border-[var(--leon-line)]" />
                <div className="text-[10px] mt-0.5 truncate">{f.leonCode || f.code || f.name}</div>
              </button>
            ))}
            {sup && !finishes.length ? <div className="col-span-full text-sm text-[var(--leon-black)]/60">Nothing with a picture matches.</div> : null}
          </div>
        </> : null}

        {src === 'drawing' ? (
          sets.length ? (
            <div className="space-y-1 max-h-80 overflow-y-auto">
              <p className="text-xs text-[var(--leon-black)]/60">
                Page one, as it stands. The drawing set itself is not touched.
              </p>
              {sets.map(d => (
                <button key={d.id} type="button" disabled={busy} onClick={() => pickDrawing(d)}
                  className="w-full text-left rounded-md border border-[var(--leon-line)] bg-white px-2 py-1.5 hover:bg-[var(--leon-cream)] disabled:opacity-50">
                  <div className="text-sm font-semibold truncate">{d.name}</div>
                  <div className="text-[11px] text-[var(--leon-black)]/55">
                    {d.revision ? 'Rev ' + d.revision : ''}{d.dateReceived ? ' \u00b7 ' + fmtDate(d.dateReceived) : ''}
                    {d.source ? ' \u00b7 ' + d.source : ''}
                  </div>
                </button>
              ))}
            </div>
          ) : <div className="text-sm text-[var(--leon-black)]/60">
                No drawing sets with a file on this job yet — they are added under Drawings.
              </div>
        ) : null}

        {src === 'project' ? (
          projectPhotos.length ? (
            <div className="grid grid-cols-4 sm:grid-cols-6 gap-2 max-h-80 overflow-y-auto">
              {projectPhotos.map((p, i) => (
                <button key={i} type="button" className="text-left"
                  onClick={() => onPick({ source: 'project', url: p.url, name: p.name })}>
                  <img src={p.url} alt={p.name} className="w-full h-20 object-cover rounded-md border border-[var(--leon-line)]" />
                  <div className="text-[10px] mt-0.5 truncate">{p.name}</div>
                </button>
              ))}
            </div>
          ) : <div className="text-sm text-[var(--leon-black)]/60">This job has no photo yet — add one on the project header, or upload here.</div>
        ) : null}

        {src === 'upload' ? (
          <div className="space-y-2">
            <input type="file" accept="image/*" onChange={onUpload} className="text-sm" />
            <p className="text-xs text-[var(--leon-black)]/60">
              Downscaled to {QUOTE_DECK_UPLOAD_MAX_PX}px before it is stored — a picture straight off a
              phone is several megabytes and the whole app's saved state has about thirteen to work with.
            </p>
            {busy ? <div className="text-sm">Processing…</div> : null}
          </div>
        ) : null}
        {busy && src === 'drawing' ? <div className="text-sm">Reading the drawing…</div> : null}
        {notice ? <div className="rounded-md bg-[var(--leon-yellow)]/20 border border-[var(--leon-yellow)]/40 p-2 text-xs">{notice}</div> : null}
      </div>
    </Modal>
  );
}

// ── Issuing a quotation on the way out ──────────────────────────────────────
// A revision that has been sent to a client and can still be edited is the one
// genuinely dangerous state in this whole module: the client holds a version
// that no longer exists anywhere in the Hub, and nothing on either side says
// so. Sending is therefore where a revision is offered the chance to freeze.
//
// It ASKS rather than doing it silently. Sending a draft for a sanity check is
// a real thing people do, and a send that quietly locked the quotation would be
// worse than the problem. Saying no is recorded on the share, so "we sent them
// an unissued draft" is answerable later.
function IssueBeforeSendModal({ open, onClose, qa, verb, onIssue, onSendAnyway }) {
  if (!qa) return null;
  return (
    <Modal open={open} onClose={onClose} size="lg"
      title={`This quotation is still a Draft`}>
      <div className="space-y-4">
        <p className="text-sm text-[var(--leon-black)]/70">
          <b>{qa.name}</b> is Rev {qa.revision || 1}, and it can still be edited. If you {verb} it now and
          change it afterwards, the client is holding a version that exists nowhere &mdash; not in the Hub,
          not in the change log, and not in what you would quote from tomorrow.
        </p>
        <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3 space-y-1.5">
          <p className="text-sm font-semibold">Issuing it as Rev {qa.revision || 1}</p>
          <ul className="text-[13px] text-[var(--leon-black)]/65 list-disc pl-4 space-y-1">
            <li>Makes it <b>read-only</b> &mdash; which is the whole reason a revision is worth having.</li>
            <li>Stamps today&rsquo;s date and your name on it as the version of record.</li>
            <li>Editing afterwards forks <b>Rev {(qa.revision || 1) + 1}</b> and reopens the wizard;
                Rev {qa.revision || 1} stays exactly as the client received it.</li>
          </ul>
        </div>
        <div className="flex flex-wrap justify-end gap-2 pt-1">
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          {/* Kept, deliberately: a draft sent for a sanity check is real work,
              and the share log records that it went out unissued. */}
          <Button variant="ghost" onClick={onSendAnyway}>
            {verb.charAt(0).toUpperCase() + verb.slice(1)} as a draft
          </Button>
          <Button onClick={onIssue}>Issue as Rev {qa.revision || 1} and {verb}</Button>
        </div>
      </div>
    </Modal>
  );
}

// The client document into LEON Sheets. Built from the SAME whitelisted
// document the screen shows, never from the analysis, so the spreadsheet cannot
// carry a column the preview did not.
function clientQuoteToSheet(ctx, project, qa, doc) {
  if (typeof ctx.exportToSheet !== 'function') return null;
  const money = '#,##0.00';
  const head = doc.detail === 'line'
    ? ['Scope', 'Item', 'Selection', 'Area', 'Qty', 'Unit', 'Price']
    : ['Scope', 'Subtotal', 'Average per unit', 'Taxes', 'Contract value'];
  const rows = [head.map(h => ({ v: h, bold: true }))];
  doc.sections.forEach(sec => {
    if (doc.detail === 'line') {
      rows.push([{ v: sec.name, bold: true }, '', '', '', '', '', { v: sec.subtotal, bold: true, fmt: money }]);
      sec.lines.forEach(l => rows.push(['', l.description,
        (l.selections || (l.selection ? [l.selection] : [])).map(f => f.name).join(' · '),
        l.area, Number(l.qty) || 0, l.uom, { v: l.price, fmt: money }]));
    } else {
      rows.push([sec.name, { v: sec.subtotal, fmt: money },
        { v: sec.averagePerUnit || 0, fmt: money }, { v: sec.taxes, fmt: money },
        { v: sec.contractValue, fmt: money }]);
    }
  });
  rows.push([]);
  rows.push(doc.detail === 'line'
    ? [{ v: 'TOTAL', bold: true }, '', '', '', '', '', { v: doc.contractValue, bold: true, fmt: money }]
    : [{ v: 'TOTAL', bold: true }, { v: doc.subtotal, bold: true, fmt: money }, '',
       { v: doc.taxes, bold: true, fmt: money }, { v: doc.contractValue, bold: true, fmt: money }]);
  return ctx.exportToSheet({
    name: `${project.name} — Quotation Rev ${doc.revision}`,
    sheetName: 'Quotation', projectId: project.id, source: 'a client quotation',
    rows, colWidths: doc.detail === 'line' ? [20, 34, 22, 16, 9, 8, 14] : [26, 15, 17, 13, 16],
  });
}
function QuoteOverviewPanel({ qa }) {
  const groups = [
    { key: 'supply', label: 'Supply', icon: '📦' },
    { key: 'labor', label: 'Labor', icon: '🔨' },
    { key: 'combined', label: 'Supply & Install', icon: '🧱' },
  ];
  const t = quoteAnalysisTotals(qa);
  return (
    <div className="space-y-4">
      {groups.map(g => {
        const secs = (qa.sections || []).filter(s => s.kind === g.key);
        if (!secs.length) return null;
        const sub = secs.reduce((a, s) => {
          const st = quoteSectionTotals(s, qa);
          return { cost: a.cost + st.cost, sell: a.sell + st.sell, lines: a.lines + st.lines };
        }, { cost: 0, sell: 0, lines: 0 });
        return (
          <div key={g.key} className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
            <div className="px-4 py-2 bg-[var(--leon-cream)] flex items-center gap-2 border-b border-[var(--leon-line)]">
              <span aria-hidden="true">{g.icon}</span>
              <span className="font-bold">{g.label}</span>
              <span className="text-xs text-[var(--leon-black)]/50">{secs.length} scope{secs.length === 1 ? '' : 's'}</span>
              <span className="ml-auto text-sm">
                <span className="text-[var(--leon-black)]/50 mr-3">{fmtMoney(sub.cost)} cost</span>
                <span className="font-bold text-[var(--leon-brown)]">{fmtMoney(sub.sell)}</span>
              </span>
            </div>
            <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="px-4 py-1.5">Scope</th>
                  <th className="px-2 py-1.5 text-right">Qty</th>
                  <th className="px-2 py-1.5">UoM</th>
                  <th className="px-2 py-1.5">Priced by</th>
                  <th className="px-2 py-1.5 text-right">Cost</th>
                  <th className="px-2 py-1.5 text-right">Sell</th>
                  <th className="px-4 py-1.5 text-right">Margin</th>
                </tr>
              </thead>
              <tbody>
                {secs.map(s => {
                  const st = quoteSectionTotals(s, qa);
                  const byUnit = quoteMethodFor({}, s, qa) === 'unitPrice';
                  return (
                    <tr key={s.id} className="border-b border-[var(--leon-line)]/60">
                      <td className="px-4 py-1.5 font-medium">{s.name}</td>
                      <td className="px-2 py-1.5 text-right tabular-nums">{qty4(st.qty)}</td>
                      <td className="px-2 py-1.5 text-[var(--leon-black)]/55">{s.uom}</td>
                      <td className="px-2 py-1.5 text-[var(--leon-black)]/55 text-xs">
                        {byUnit ? `$${s.unitPrice || 0} / ${s.uom || 'unit'}`
                                : `${pct(s.ratePct === null ? qa.defaultRatePct : s.ratePct)} ${quoteRateBasisFor(null, s, qa) === 'markup' ? 'markup' : 'margin'}`}
                      </td>
                      <td className="px-2 py-1.5 text-right tabular-nums">{fmtMoney(st.cost)}</td>
                      <td className="px-2 py-1.5 text-right tabular-nums font-semibold text-[var(--leon-brown)]">{fmtMoney(st.sell)}</td>
                      <td className={`px-4 py-1.5 text-right tabular-nums ${st.marginPct < 0.18 && st.sell ? 'text-red-600 font-semibold' : ''}`}>
                        {st.sell ? pct(st.marginPct) : '—'}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        );
      })}

      {!(qa.sections || []).length && <EmptyState text="No scopes in this draft yet. Add them under Supply or Labor." />}

      {!!(qa.sections || []).length && (
        <div className="rounded-lg border border-[var(--leon-brown)] bg-[var(--leon-brown)] text-white p-4 flex items-center gap-6 flex-wrap">
          <div>
            <div className="text-[10px] uppercase tracking-wide opacity-70">Total cost</div>
            <div className="text-lg font-semibold">{fmtMoney(t.cost)}</div>
          </div>
          <div>
            <div className="text-[10px] uppercase tracking-wide opacity-70">Total sell</div>
            <div className="text-2xl font-bold">{fmtMoney(t.sell)}</div>
          </div>
          <div>
            <div className="text-[10px] uppercase tracking-wide opacity-70">Margin</div>
            <div className="text-lg font-semibold">{fmtMoney(t.margin)} · {pct(t.marginPct)}</div>
          </div>
          <div className="ml-auto text-right text-[11px] opacity-80">
            {t.lines} priced line{t.lines === 1 ? '' : 's'} across {t.sections} scope{t.sections === 1 ? '' : 's'}
            {t.excluded ? ` · ${t.excluded} carried but not priced` : ''}
            {t.unpriced ? ` · ${t.unpriced} still unpriced` : ''}
          </div>
        </div>
      )}
    </div>
  );
}

// The cost recipe for one scope: what it counts, how its material is bought,
// how many fit a container and what the three freight legs cost. Collapsed by
// default — it is set once per scope and then left alone — but never hidden,
// because a rate nobody can see is how the workbook's constants became
// invisible in the first place.
function QuoteScopeRecipe({ sec, qa, set, editable }) {
  const basis = sec.freightBasis || qa.freightBasis;
  const byContainer = basis === 'container';
  const bySlab = (sec.matBasis || 'unit') === 'slab';
  const recipe = quoteRecipeFor(sec.name || sec.scopeKey);
  return (
    <Collapsible id={`qa-recipe-${sec.id}`} title="Cost recipe"
      right={<span className="text-[11px] text-[var(--leon-black)]/45">
        {(QUOTE_COST_DRIVERS.find(d => d.key === (sec.costDriver || 'qty')) || {}).label}
        {byContainer && sec.containerCapacity ? ` · ${qty4(sec.containerCapacity)} per container` : ''}
        {bySlab && sec.slabYield ? ` · ${sec.slabYield} per slab` : ''}
      </span>}>
      <div className="grid sm:grid-cols-3 lg:grid-cols-4 gap-3">
        <Field label="What this scope counts" hint="What the container fraction is measured against.">
          <Select value={sec.costDriver || 'qty'} disabled={!editable}
            onChange={e => set({ costDriver: e.target.value })}>
            {QUOTE_COST_DRIVERS.map(d => <option key={d.key} value={d.key}>{d.label}</option>)}
          </Select>
        </Field>
        {(sec.costDriver === 'piece') && (
          <Field label="Stick length" hint="Inches. Linear feet become this many pieces.">
            <QNum w="w-24" value={sec.pieceLengthIn} disabled={!editable} onChange={v => editable && set({ pieceLengthIn: v })} />
          </Field>
        )}
        <Field label="How material is bought">
          <Select value={sec.matBasis || 'unit'} disabled={!editable}
            onChange={e => set({ matBasis: e.target.value })}>
            {QUOTE_MATERIAL_BASES.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
          </Select>
        </Field>
        {bySlab && (
          <>
            <Field label="Yield per slab" hint="Usable units one slab gives.">
              <QNum w="w-24" value={sec.slabYield} disabled={!editable} onChange={v => editable && set({ slabYield: v })} />
            </Field>
            <Field label="Waste" hint="Added before the slab count is worked out.">
              <QPct w="w-20" value={sec.slabWastePct} disabled={!editable} onChange={v => editable && set({ slabWastePct: v })} />
            </Field>
            <Field label="Slab rate">
              <QNum w="w-28" prefix="$" value={sec.slabRate} disabled={!editable} onChange={v => editable && set({ slabRate: v })} />
            </Field>
          </>
        )}
        <Field label="Overhead" hint="On material, before freight.">
          <QPct w="w-20" placeholder={String(Math.round((qa.overheadPct || 0) * 100))}
            value={sec.overheadPct} disabled={!editable} onChange={v => editable && set({ overheadPct: v })} />
        </Field>
      </div>

      {byContainer ? (
        <div className="grid sm:grid-cols-4 gap-3 mt-3 pt-3 border-t border-[var(--leon-line)]">
          <Field label="Fit one container" hint="How many of the driver above.">
            <QNum w="w-28" value={sec.containerCapacity} disabled={!editable} onChange={v => editable && set({ containerCapacity: v })} />
          </Field>
          <Field label="Ocean freight" hint="Per container.">
            <QNum w="w-28" prefix="$" value={sec.freightPerContainer} disabled={!editable} onChange={v => editable && set({ freightPerContainer: v })} />
          </Field>
          <Field label="Inland" hint="Per container.">
            <QNum w="w-28" prefix="$" value={sec.inlandPerContainer} disabled={!editable} onChange={v => editable && set({ inlandPerContainer: v })} />
          </Field>
          <Field label="Broker" hint="Per container.">
            <QNum w="w-28" prefix="$" value={sec.brokerPerContainer} disabled={!editable} onChange={v => editable && set({ brokerPerContainer: v })} />
          </Field>
          <p className="sm:col-span-4 text-[11px] text-[var(--leon-black)]/50">
            A line is charged its <strong>fraction</strong> of a container, never rounded up &mdash; rounding
            each line up would charge a full container once per line on a job that ships them together.
            {recipe && <> The defaults come from this scope&rsquo;s own figures in the Revere Street workbook.</>}
          </p>
        </div>
      ) : (
        <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
          This scope charges freight as <strong>{(QUOTE_FREIGHT_BASES.find(b => b.key === basis) || {}).label || basis}</strong>.
          Switch it to &ldquo;By the container&rdquo; above to set a capacity and the three freight legs.
        </p>
      )}
    </Collapsible>
  );
}

// QuoteSalesPointsStrip lived here: the job's nine sales points, stated above
// the scopes. Removed on the client's instruction — the sales information for a
// scope belongs to that scope, and a block read before any of them is a step in
// front of the work. Its two actions moved onto the quotation's action row.
function QuoteSectionCard({ ctx, project, qa, sec, editable, density }) {
  const full = density !== 'essentials';
  // The columns a scope actually needs. A Supply Only scope has no
  // installation, a Labor scope has no vendor, freight or duty — showing those
  // columns anyway is what made the table read as a wall of dashes.
  const isLabor = sec.kind === 'labor';
  const isCombined = sec.kind === 'combined';
  // The cost band has to account for every dollar in the Cost column, or the
  // row reads as arithmetic that does not close — which is exactly what a
  // reviewer notices first. A supply scope should carry no install and no
  // labour, so those columns are absent; if a line does carry one (imported, or
  // priced before the supply/labour split), the column APPEARS rather than the
  // money hiding inside Cost.
  const anyInstall = !isLabor && sec.lines.some(l => !l.excluded && qnum(l.installUnit));
  const anyLabor = !isLabor && sec.lines.some(l => !l.excluded && qnum(l.laborUnit));
  // Essentials folds overhead, freight, duty, install and labour into Cost;
  // the line's own detail panel still spells every one of them out, so nothing
  // is hidden, only un-columned.
  const showInstall = full && (isCombined || anyInstall);
  const showLabor = full && anyLabor;
  const showCharges = full && !isLabor;
  // Freight is read the way the item is read: a RATE and a TOTAL, side by side,
  // so "what does shipping cost per unit" is answerable off the row instead of
  // by opening the line. That is 4 charge columns, not 3.
  const costCols = isLabor ? 2 : (2 + (showCharges ? 4 : 0) + (showInstall ? 1 : 0) + (showLabor ? 1 : 0));
  const earnCols = full ? 5 : 4;
  const totalCols = 7 + costCols + earnCols + 1;
  // The scope's cost driver renames the quantity columns, so the header asks
  // for the thing this trade actually counts.
  const driver = sec.costDriver || 'qty';
  const perLabel = driver === 'module' ? 'Modules / item' : driver === 'piece' ? 'Length / item' : 'Qty / item';
  const driverWord = driver === 'module' ? 'modules' : 'qty';
  const st = quoteSectionTotals(sec, qa);
  const set = f => ctx.updateQuoteSection(project.id, qa.id, sec.id, f);
  const byUnit = quoteMethodFor({}, sec, qa) === 'unitPrice';
  // What the closed Pricing disclosure says, so how a scope is priced can be
  // read without opening anything.
  const priceSummary = [
    byUnit
      ? `${fmtMoney(qnum(sec.unitPrice))} / ${sec.uom || 'unit'}`
      : `${pct(qnum(sec.ratePct != null ? sec.ratePct : qa.defaultRatePct))} ${quoteRateBasisFor(null, sec, qa) === 'markup' ? 'markup on cost' : 'margin on sell'}${sec.ratePct === null ? ' (job)' : ''}`,
    isLabor ? null : `freight ${quoteChargeLabel({}, sec, qa, 'freight')}`,
    isLabor ? null : `duty ${quoteChargeLabel({}, sec, qa, 'duty')}`,
    `${qty4(st.qty)} ${sec.uom || ''}`.trim(),
  ].filter(Boolean).join(' \u00b7 ');

  return (
    <Collapsible id={`qa-sec-${sec.id}`} defaultOpen title={sec.name || 'Untitled scope'} count={sec.lines.length}
      right={
        <span className="flex items-center gap-3 text-xs">
          <span className="text-[var(--leon-black)]/50">{fmtMoney(st.cost)} cost</span>
          <span className="font-bold text-[var(--leon-brown)]">{fmtMoney(st.sell)}</span>
          <Badge tone={marginTone(st.marginPct)}>{pct(st.marginPct)}</Badge>
        </span>
      }>
      <div className="space-y-3">
        {/* A scope reads in three parts, and they are numbered because they are
            done in order: what it sells for, what is in it, what it comes to.
            The sales information belongs to THIS scope — the job carries only
            the defaults it starts from. */}
        <QuoteBand id={`qa-sec-price-${sec.id}`} n="1" tone="sales" title="Sales info"
          right={priceSummary}>
          <div className="flex flex-wrap items-end gap-3">
          <Field label="How this scope is priced" className="min-w-[190px]">
            <Select value={sec.pricingMethod || 'rate'} disabled={!editable}
              onChange={e => set({ pricingMethod: e.target.value })}>
              {QUOTE_PRICING_METHODS.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
            </Select>
          </Field>
          {byUnit ? (
            <Field label={`Unit price${sec.uom ? ` / ${sec.uom}` : ''}`} hint="The margin is whatever this earns.">
              <QNum w="w-28" prefix="$" value={sec.unitPrice} disabled={!editable} onChange={v => editable && set({ unitPrice: v })} />
            </Field>
          ) : sec.pricingMethod === 'sell' ? (
            <Field label="Sell price" hint="What the client is being quoted. The margin is whatever it earns.">
              <QNum w="w-32" prefix="$" value={sec.sellPrice} disabled={!editable} onChange={v => editable && set({ sellPrice: v })} />
            </Field>
          ) : (
            <Field label={quoteRateBasisFor(null, sec, qa) === 'markup' ? 'Markup on cost' : 'Margin on sell'}
              hint={sec.ratePct === null ? `Following the job, ${pct(qa.defaultRatePct)}` : 'Set for this scope'}>
              {/* The job's tier is the default; a scope may sit on a different
                  one, or on a number of its own. Clearing it follows the job
                  again — null and 0 are different answers. */}
              <div className="flex items-center gap-1.5">
                <Select className="!w-28 !py-1 !text-xs" disabled={!editable}
                  value={(quoteMarginTierFor(sec.ratePct) || {}).key || ''}
                  onChange={e => {
                    const t = QUOTE_MARGIN_TIERS.find(x => x.key === e.target.value);
                    set({ ratePct: t ? t.rate : null });
                  }}>
                  <option value="">{sec.ratePct === null ? 'Follow job' : 'Custom'}</option>
                  {QUOTE_MARGIN_TIERS.map(t => <option key={t.key} value={t.key}>{t.label}</option>)}
                </Select>
                <QPct w="w-20" placeholder={String(Math.round(qa.defaultRatePct * 100))}
                  value={sec.ratePct} disabled={!editable} onChange={v => editable && set({ ratePct: v })} />
              </div>
              {/* Margin on sell is the house convention, so it is what an unset
                  scope inherits. This is here for the scope that genuinely is
                  bought cost-plus — 35% markup earns 25.9%, 35% margin earns
                  35%, and quoting one as the other is a real loss. */}
              <div className="flex items-center gap-1.5 mt-1">
                <Select className="!w-44 !py-0.5 !text-[11px]" disabled={!editable}
                  value={sec.rateBasis || ''} onChange={e => editable && set({ rateBasis: e.target.value || null })}>
                  <option value="">Follow job &mdash; {(QUOTE_RATE_BASES.find(b => b.key === qa.rateBasis) || {}).label}</option>
                  {QUOTE_RATE_BASES.map(b => <option key={b.key} value={b.key}>{b.label}</option>)}
                </Select>
                <span className="text-[10px] text-[var(--leon-black)]/40">
                  {(QUOTE_RATE_BASES.find(b => b.key === quoteRateBasisFor(null, sec, qa)) || {}).hint}
                </span>
              </div>
            </Field>
          )}
          <Field label="UoM" className="w-24">
            <TextInput value={sec.uom || ''} disabled={!editable} onChange={e => set({ uom: e.target.value })} />
          </Field>
          {/* Overhead is charged on material BEFORE freight, so it is part of
              the cost the margin is taken over — it belongs with the price. */}
          <Field label="Overhead" hint="On material, before freight.">
            <QPct w="w-20" placeholder={String(Math.round(qnum(qa.overheadPct) * 1000) / 10)}
              value={sec.overheadPct} disabled={!editable} onChange={v => editable && set({ overheadPct: v })} />
          </Field>
          <Field label="What this scope counts" hint="What a container fraction is measured against.">
            <Select value={sec.costDriver || 'qty'} disabled={!editable}
              onChange={e => set({ costDriver: e.target.value })}>
              {QUOTE_COST_DRIVERS.map(d => <option key={d.key} value={d.key}>{d.label}</option>)}
            </Select>
          </Field>
          {sec.costDriver === 'piece' && (
            <Field label="Stick length" hint="Inches. Linear feet become this many pieces.">
              <QNum w="w-24" value={sec.pieceLengthIn} disabled={!editable}
                onChange={v => editable && set({ pieceLengthIn: v })} />
            </Field>
          )}
          <Field label="How material is bought">
            <Select value={sec.matBasis || 'unit'} disabled={!editable}
              onChange={e => set({ matBasis: e.target.value })}>
              {QUOTE_MATERIAL_BASES.map(b => <option key={b.key} value={b.key}>{b.label}</option>)}
            </Select>
          </Field>
          {(sec.matBasis || 'unit') === 'slab' && (
            <>
              <Field label="Yield per slab" hint="Usable units one slab gives.">
                <QNum w="w-24" value={sec.slabYield} disabled={!editable} onChange={v => editable && set({ slabYield: v })} />
              </Field>
              <Field label="Waste" hint="Added before the slab count is worked out.">
                <QPct w="w-20" value={sec.slabWastePct} disabled={!editable} onChange={v => editable && set({ slabWastePct: v })} />
              </Field>
              <Field label="Slab rate">
                <QNum w="w-28" prefix="$" value={sec.slabRate} disabled={!editable} onChange={v => editable && set({ slabRate: v })} />
              </Field>
            </>
          )}
          </div>

          {/* What the SALE costs, beside what it sells for. */}
          <QuoteScopeCommissions sec={sec} qa={qa} set={set} editable={editable} />
          {/* You price a scope ONE way and you get asked about it three ways —
              margin on cost, margin on price, and what that is per unit. Stated
              together because converting between them in your head is how 35%
              markup goes out of the door sold as 35% margin. */}
          <QuoteScopeMargins st={st} sec={sec} />
        </QuoteBand>

        {/* 2 · LOGISTICS COSTS — the same band the wizard shows, from the same
            component, so setting a scope up and reviewing it cannot differ. */}
        <QuoteScopeLogistics sec={sec} qa={qa} set={set} editable={editable} ctx={ctx} />

        {/* 3 · ITEM LINES — on white, because this is the band you read across
            and the two bands above are the settings behind it. */}
        <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 pt-1 pb-1">
          3 &middot; Item lines
        </div>
        <div className="overflow-x-auto">
          <table className={`q-rows w-full text-xs ${full ? 'min-w-[1180px]' : 'min-w-[860px]'}`}>
            <thead>
              {/* Fifteen columns is only readable if it says what the bands
                  ARE. Three questions, in order: what is the item, what does it
                  cost us, what does it earn. Without this the row is a wall of
                  numbers that do not line up with anything. */}
              <tr className="text-[9px] uppercase tracking-[0.12em] text-[var(--leon-black)]/35">
                <th colSpan={7} className="py-1 pr-2 text-left border-b border-[var(--leon-line)]">The item</th>
                <th colSpan={costCols} className="py-1 pr-2 text-center border-b border-l border-[var(--leon-line)]">
                  {isLabor ? 'What the labour costs us' : 'What it costs us'}
                </th>
                <th colSpan={earnCols} className="py-1 pr-2 text-center border-b border-l border-[var(--leon-line)]">What it earns</th>
                {/* The band row must span EVERY column of the row beneath it —
                    one short and the browser lays the bands out against a
                    different column set, which is exactly what made the header
                    fail to line up with its own table. */}
                <th className="border-b border-[var(--leon-line)]"></th>
              </tr>
              <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
                <th className="py-1.5 pr-2">Item name</th>
                <th className="py-1.5 pr-2">Type</th>
                <th className="py-1.5 pr-2 min-w-[150px]">Description</th>
                <th className="py-1.5 pr-2 min-w-[130px]">Proposed finishes</th>
                {/* What the scope COUNTS decides what these two columns are
                    asking for. Casework counts modules per kitchen; trim counts
                    linear feet cut into sticks; everything else counts whatever
                    the take-off measured. A header that says "Qty" for all
                    three makes the person guess which one it wants. */}
                <th className="py-1.5 pr-2 text-right" title="How many of the counted thing in ONE item">{perLabel}</th>
                <th className="py-1.5 pr-2 text-right" title="How many identical items of this type">&times; Items</th>
                <th className="py-1.5 pr-2 text-right">Total {driverWord}</th>
                {/* A labour scope buys no material and ships nothing, so the
                    columns say labour and the freight/duty pair is dropped
                    rather than printed as three dashes on every row. */}
                <th className="py-1.5 pr-2 text-right border-l border-[var(--leon-line)]">{isLabor ? 'Labour rate' : 'Vendor rate'}</th>
                <th className="py-1.5 pr-2 text-right">{isLabor ? 'Labour total' : 'Vendor total'}</th>
                {showCharges && <th className="py-1.5 pr-2 text-right">O/H</th>}
                {/* Rate then total, exactly as the item reads. A hidden-but-
                    present <th> would break the band colSpans, so it is a
                    conditional render like every other column here. */}
                {showCharges && <th className="py-1.5 pr-2 text-right">Freight rate</th>}
                {showCharges && <th className="py-1.5 pr-2 text-right">Freight</th>}
                {showCharges && <th className="py-1.5 pr-2 text-right">Duty</th>}
                {showLabor && <th className="py-1.5 pr-2 text-right">Labour</th>}
                {showInstall && <th className="py-1.5 pr-2 text-right">Install</th>}
                <th className="py-1.5 pr-2 text-right border-l border-[var(--leon-line)]">Cost</th>
                <th className="py-1.5 pr-2 text-center">Price by</th>
                <th className="py-1.5 pr-2 text-right">Sell</th>
                {full && <th className="py-1.5 pr-2 text-right">Profit</th>}
                <th className="py-1.5 pr-2 text-right">CM</th>
                <th className="py-1.5 w-8"></th>
              </tr>
            </thead>
            <tbody>
              {sec.lines.map((line, i) => (
                <QuoteLineRow key={line.id} ctx={ctx} project={project} qa={qa} sec={sec} line={line}
                  editable={editable} showInstall={showInstall} showLabor={showLabor}
                  showCharges={showCharges} full={full} alt={i % 2 === 1} />
              ))}
              {!sec.lines.length && (
                <tr><td colSpan={totalCols} className="py-4 text-center text-[var(--leon-black)]/40">No lines yet.</td></tr>
              )}
            </tbody>
            {!!st.lines && (
              <tfoot>
                <tr className="border-t-2 border-[var(--leon-line)] font-semibold">
                  <td colSpan={6} className="py-2 pr-2 text-right text-[var(--leon-black)]/60">Scope total</td>
                  <td className="py-2 pr-2 text-right tabular-nums">{qty4(st.qty)}</td>
                  <td className="py-2 border-l border-[var(--leon-line)]"></td>
                  <td className="py-2 pr-2 text-right tabular-nums">{fmtMoney(isLabor ? st.labor : st.mat)}</td>
                  {showCharges && <td className="py-2 pr-2 text-right tabular-nums text-[var(--leon-black)]/60">{st.overhead ? fmtMoney(st.overhead) : '—'}</td>}
                  {showCharges && (
                    <td className="py-2 pr-2 text-right tabular-nums italic text-[var(--leon-black)]/45">
                      {st.qty && st.freight ? fmtMoney(st.freight / st.qty) : '—'}
                    </td>
                  )}
                  {showCharges && <td className="py-2 pr-2 text-right tabular-nums text-[var(--leon-black)]/60">{st.freight ? fmtMoney(st.freight) : '—'}</td>}
                  {showCharges && <td className="py-2 pr-2 text-right tabular-nums text-[var(--leon-black)]/60">{st.duty ? fmtMoney(st.duty) : '—'}</td>}
                  {showLabor && <td className="py-2 pr-2 text-right tabular-nums">{fmtMoney(st.labor)}</td>}
                  {showInstall && <td className="py-2 pr-2 text-right tabular-nums">{fmtMoney(st.install)}</td>}
                  <td className="py-2 pr-2 text-right tabular-nums border-l border-[var(--leon-line)]">{fmtMoney(st.cost)}</td>
                  <td></td>
                  <td className="py-2 pr-2 text-right tabular-nums text-[var(--leon-brown)]">{fmtMoney(st.sell)}</td>
                  {full && <td className="py-2 pr-2 text-right tabular-nums">{fmtMoney(st.profit)}</td>}
                  <td className="py-2 pr-2 text-right tabular-nums">{pct(st.cm)}</td>
                  <td></td>
                </tr>
              </tfoot>
            )}
          </table>
        </div>

        {editable && (
          <div className="flex items-center gap-3">
            <button onClick={() => ctx.addQuoteLine(project.id, qa.id, sec.id)}
              className="text-xs font-semibold text-[var(--leon-brown)]">+ Add a line</button>
            {/* A quotation is not only priced items. An area heading breaks a
                long scope into the parts of the building it is for, and a note
                says something about the lines around it. Both are rows in the
                same list, so they order with the work instead of collecting at
                the bottom of the scope. */}
            {QUOTE_ROW_KINDS.map(k => (
              <button key={k.key} title={k.hint}
                onClick={() => ctx.addQuoteLine(project.id, qa.id, sec.id, { rowKind: k.key })}
                className="text-xs font-semibold text-[var(--leon-black)]/50 hover:text-[var(--leon-brown)]">
                + Add {k.label === 'area' ? 'an' : 'a'} {k.label}
              </button>
            ))}
            <button onClick={() => { if (confirm(`Remove the "${sec.name}" scope and its ${sec.lines.length} line(s) from this draft?`)) ctx.removeQuoteSection(project.id, qa.id, sec.id); }}
              className="text-xs font-semibold text-red-600 ml-auto">Remove scope</button>
          </div>
        )}

        {/* 3 · What the scope comes to. Sales tax is NOT entered here: it is
            the job's rate, applied to every scope automatically, because tax
            follows the jurisdiction the work is installed in rather than the
            scope. A rate typed per scope would be four copies of one fact. */}
        <QuoteScopeTotal project={project} qa={qa} st={st} />
      </div>
    </Collapsible>
  );
}

// The third part of a scope: subtotal, tax, total. Read-only on purpose —
// every figure here is computed from the lines above and the job's tax rate,
// and a total you can type over is a total that stops agreeing with its lines.
// The three ways a scope's price gets asked about, from ONE set of figures, so
// they cannot disagree. All of it is computed in quoteSectionTotals — the view
// does no arithmetic of its own, which is the rule that stopped the cut count
// and the cut list giving two different answers.
// What the SALE costs, as opposed to what the goods cost. All three are charged
// on the client price and all three come out of profit, so they are asked
// together and away from the landed-cost band. Blank follows the job; 0 is a
// real answer meaning none — collapsing those two is how a scope that should
// pay no referral quietly starts paying one.
function QuoteScopeCommissions({ sec, qa, set, editable }) {
  const f = (label, key, hint) => (
    <Field label={label} hint={hint}>
      <QPct w="w-20" placeholder={String(Math.round((qnum(qa[key]) || 0) * 100))}
        value={sec[key]} disabled={!editable} onChange={v => editable && set({ [key]: v })} />
    </Field>
  );
  const follows = k => sec[k] === null || sec[k] === undefined;
  return (
    <div className="flex flex-wrap items-end gap-3 mt-3 pt-3 border-t border-[var(--leon-line)]">
      {f('Sales commission', 'commissionPct',
        follows('commissionPct') ? `Following the job, ${pct(qa.commissionPct)}` : 'Set for this scope')}
      {f('GM bonus', 'bonusPct',
        follows('bonusPct') ? `Following the job, ${pct(qa.bonusPct)}` : 'Set for this scope')}
      {/* A referral is a share of the price OR a flat sum for the
          introduction. A percentage inherits like every other rate; a flat sum
          is charged ONCE on the scope and never pushed down to the lines,
          which would multiply it by however many there are. */}
      <Field label="Referral fee">
        <div className="flex items-center gap-1.5">
          <Select className="!w-24 !py-1 !text-xs" disabled={!editable}
            value={sec.referralBasis || ''}
            onChange={e => editable && set({ referralBasis: e.target.value || null })}>
            <option value="">Follow job</option>
            <option value="pct">% of price</option>
            <option value="amount">Set amount</option>
          </Select>
          {(qpick(sec.referralBasis, qa.referralBasis) || 'pct') === 'amount'
            ? <QNum w="w-28" prefix="$" value={sec.referralAmt} disabled={!editable}
                onChange={v => editable && set({ referralAmt: v })} />
            : <QPct w="w-20" placeholder={String(Math.round(qnum(qa.referralPct) * 1000) / 10)}
                value={sec.referralPct} disabled={!editable}
                onChange={v => editable && set({ referralPct: v })} />}
        </div>
        <div className="text-[10px] text-[var(--leon-black)]/45 mt-0.5">
          {(qpick(sec.referralBasis, qa.referralBasis) || 'pct') === 'amount'
            ? 'Charged once on this scope, not per line.'
            : follows('referralPct')
              ? (qnum(qa.referralPct) ? `Following the job, ${pct(qa.referralPct)}${qa.referralTo ? ` to ${qa.referralTo}` : ''}` : 'None on this job')
              : 'Set for this scope'}
        </div>
      </Field>
      <p className="text-[11px] text-[var(--leon-black)]/45 basis-full">
        All three are charged on the client price and come out of profit &mdash; the client is not
        billed more because of them.
      </p>
    </div>
  );
}

// Freight by the container is a CHAIN, not a rate, so it is stated as one:
// what the scope carries, how many fill a box, what therefore ships.
function QuoteScopeContainerChain({ st, sec }) {
  if (st.containers === null || st.containers === undefined) return null;
  const driverKey = sec.costDriver || 'qty';
  const word = driverKey === 'module' ? 'modules'
    : driverKey === 'piece' ? 'pieces' : (sec.uom || 'units');
  return (
    <p className="text-[11px] text-[var(--leon-black)]/60 mt-3 pt-3 border-t border-[var(--leon-line)] tabular-nums">
      <strong>Freight by the container:</strong>{' '}
      {qnum(st.driverQty).toLocaleString()} {word}
      {' \u00f7 '}{qnum(st.containerCapacity).toLocaleString()} per container{' = '}
      <strong>{Math.round(st.containers * 100) / 100} container{st.containers === 1 ? '' : 's'}</strong>
      {' \u2014 '}charged as the fraction, never rounded up per line.
    </p>
  );
}

function QuoteScopeMargins({ st, sec }) {
  if (!st.lines) return null;
  const cell = (k, v, sub) => (
    <div className="min-w-0">
      <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40 truncate">{k}</div>
      <div className="font-semibold text-sm tabular-nums">{v}</div>
      <div className="text-[10px] text-[var(--leon-black)]/35 truncate" title={sub}>{sub}</div>
    </div>
  );
  return (
    <div className="mt-3 rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/50 px-3 py-2">
      <div className="grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-2">
        {cell('Margin from cost', pct(st.marginOnCostPct), 'markup — margin \u00f7 cost')}
        {cell('Margin from price', pct(st.marginPct), 'on sell — margin \u00f7 price')}
        {cell('Price per unit', st.qty ? fmtMoney(st.unitSell) : '\u2014',
          st.qty ? `${qnum(st.qty).toLocaleString()} ${sec.uom || 'units'} \u00b7 cost ${fmtMoney(st.unitCost)}` : 'no quantity yet')}
        {cell('Scope sell', fmtMoney(st.sell), `cost ${fmtMoney(st.cost)}`)}
      </div>
    </div>
  );
}

function QuoteScopeTotal({ project, qa, st }) {
  const jobRate = typeof projectTaxRate === 'function' ? projectTaxRate(project) : 0;
  const rate = (qa.taxRatePct === null || qa.taxRatePct === undefined) ? jobRate : qnum(qa.taxRatePct);
  const exempt = project && project.taxExempt;
  const tax = exempt ? 0 : st.sell * rate;
  const row = (label, value, note, strong) => (
    <div className={`flex items-baseline gap-2 ${strong ? 'pt-1.5 border-t border-[var(--leon-line)]' : ''}`}>
      <span className={strong ? 'font-bold text-sm' : 'text-[13px] text-[var(--leon-black)]/65'}>{label}</span>
      {note && <span className="text-[11px] text-[var(--leon-black)]/40">{note}</span>}
      <span className="flex-1" />
      <span className={`tabular-nums ${strong ? 'font-bold text-base' : 'text-[13px]'}`}>{value}</span>
    </div>
  );
  return (
    <div className="mt-3">
      <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/45 mb-1.5">
        4 &middot; Scope total
      </div>
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 px-3 py-2 space-y-1 max-w-md ml-auto">
        {row('Subtotal', fmtMoney(st.sell), `${st.lines} line${st.lines === 1 ? '' : 's'}`)}
        {row(exempt ? 'Sales tax — exempt' : `Sales tax ${pct(rate)}`, fmtMoney(tax),
          exempt ? 'this job is tax exempt'
                 : (qa.taxRatePct === null || qa.taxRatePct === undefined)
                   ? 'the job\u2019s rate' : 'set on this quotation')}
        {row('Scope total', fmtMoney(st.sell + tax), null, true)}
        {!!st.unpriced && (
          <p className="text-[11px] text-[var(--leon-red)] pt-1">
            {st.unpriced} line{st.unpriced === 1 ? '' : 's'} still unpriced, so this total is not complete.
          </p>
        )}
      </div>
    </div>
  );
}

// Every cost on a line, shown as the arithmetic that produced it. The item
// columns already read as "rate x how many = total"; freight, duty and overhead
// are charged the same way and were previously a single figure with no working,
// which is the one number on a quote nobody can check. A container is the case
// that matters: what ships, how many fit one box, the fraction that leaves, and
// three separate legs charged against that fraction.
// The tariff library is a few thousand code/country pairs. Rendering it as a
// <select> put hundreds of options into the DOM of every expanded line and gave
// the person a list nobody can read down. Country first — which is what an
// estimator actually knows — then a search over code and description, showing a
// handful of matches. Nothing is picked until a match is clicked.
// What is being offered on a line, picked from the real finish catalogs rather
// than typed. A line can carry SEVERAL — a door is a core plus a laminate plus
// an edge — so this is a list, and the free-text field is kept alongside it for
// anything that has no catalog record yet (a client\u2019s own specification, a
// sample still being chased).
function quoteLineFinishes(line) {
  if (Array.isArray(line.finishRefs)) return line.finishRefs;
  return line.finishRef ? [line.finishRef] : [];
}
function QuoteFinishCell({ ctx, line, set, editable }) {
  const [open, setOpen] = useState(false);
  const chosen = quoteLineFinishes(line);
  return (
    <>
      <div className="flex items-center gap-1 flex-wrap">
        {chosen.map(f => (
          <span key={f.source + ':' + f.id} title={`${f.name}${f.code ? ` \u00b7 ${f.code}` : ''}`}
            className="inline-flex items-center gap-1 max-w-[120px] rounded border border-[var(--leon-line)] bg-white pl-0.5 pr-1.5 py-0.5">
            {f.img
              ? <img src={f.img} alt="" loading="lazy" className="w-4 h-4 rounded-sm object-cover" />
              : <span className="w-4 h-4 rounded-sm bg-[var(--leon-cream)] border border-[var(--leon-line)]" />}
            <span className="text-[10px] truncate">{f.name}</span>
          </span>
        ))}
        {!chosen.length && line.proposedFinishes && (
          <span className="text-[11px] text-[var(--leon-black)]/60 truncate">{line.proposedFinishes}</span>
        )}
        {editable && (
          <button type="button" onClick={() => setOpen(true)}
            className="text-[11px] font-semibold text-[var(--leon-brown)] hover:underline whitespace-nowrap">
            {chosen.length || line.proposedFinishes ? 'Edit' : '+ Finish'}
          </button>
        )}
      </div>
      <QuoteFinishModal ctx={ctx} open={open} onClose={() => setOpen(false)} line={line} set={set} />
    </>
  );
}
// One specification answer: picked from Supplier Finishes, or typed.
//
// The VALUE stored in `line.specs[name]` is always plain text — the product's
// own name when one is picked — because that is what travels to the client
// document, which copies `specs` wholesale. The reference behind it lives in
// `line.specRefs[name]` and carries the supplier, the code and the vendor id.
// Keeping them apart is what stops "pick it from the catalog" quietly handing
// a client our sourcing: the whitelist copies specs and has never heard of
// specRefs, so the private half fails closed.
// What a line is being quoted on, read from its area. Stated, never re-asked —
// a line's specification now comes from the heading above it, and the one thing
// a line still has to do is show what that is.
function QuoteLineSpecSummary({ sec, line, fields }) {
  if (!fields || !fields.length) return null;
  const res = quoteResolvedSpecs(sec, line);
  const answered = fields.filter(f => res.specs[f]);
  if (!res.area) {
    return (
      <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
        No area heading above this line, so it carries no specification.
        Add an area and answer it there.
      </p>
    );
  }
  return (
    <div className="mt-2 pt-2 border-t border-[var(--leon-line)]">
      <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">
        Specification &mdash; from {res.area.description || 'the area above'}
      </div>
      {!answered.length ? (
        <p className="text-[11px] text-[var(--leon-black)]/45">
          Nothing answered on that area yet.
        </p>
      ) : (
        <div className="flex flex-wrap gap-x-4 gap-y-1">
          {answered.map(f => (
            <span key={f} className="text-[11px]">
              <span className="text-[var(--leon-black)]/45">{f}: </span>
              <span className="font-semibold">{res.specs[f]}</span>
              {res.refs[f] && res.refs[f].img && (
                <img src={res.refs[f].img} alt="" className="inline-block w-4 h-4 rounded object-cover ml-1 align-[-3px]" />
              )}
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

function QuoteSpecField({ ctx, name, line, set, editable, inherited }) {
  const [picking, setPicking] = useState(false);
  const specs = line.specs || {};
  const refs = line.specRefs || {};
  const ref = refs[name] || null;
  const write = (text, newRef) => set({
    specs: Object.assign({}, specs, { [name]: text }),
    specRefs: Object.assign({}, refs, { [name]: newRef || null }),
  });
  return (
    <Field label={name}>
      <div className="flex items-center gap-1">
        {/* An inherited answer shows as a PLACEHOLDER, never as a value: a
            field that looks filled in but is really the area's answer is how a
            line ends up quoted on a finish nobody chose for it. */}
        <TextInput value={specs[name] || ''} disabled={!editable}
          placeholder={inherited || ''}
          onChange={e => write(e.target.value, null)} />
        {editable && (
          <button type="button" onClick={() => setPicking(true)} title={`Pick ${name} from Supplier Finishes`}
            className="shrink-0 px-1.5 py-1 rounded border border-[var(--leon-line)] text-[11px] hover:bg-[var(--leon-cream)]">
            {'\u{1F3A8}'}
          </button>
        )}
      </div>
      {ref && (
        <div className="flex items-center gap-1.5 mt-1 min-w-0">
          {ref.img && <img src={ref.img} alt="" className="w-5 h-5 rounded object-cover shrink-0" />}
          <span className="text-[10px] text-[var(--leon-black)]/45 truncate"
            title={`${ref.supLabel || ref.source}${ref.code ? ` \u00b7 ${ref.code}` : ''}`}>
            {ref.supLabel || ref.source}{ref.code ? ` \u00b7 ${ref.code}` : ''}
          </span>
          {editable && (
            <button type="button" onClick={() => write(specs[name] || '', null)}
              title="Unlink the catalog product, keep the text"
              className="text-[var(--leon-black)]/25 hover:text-[var(--leon-red)] text-[10px] shrink-0">&#10005;</button>
          )}
        </div>
      )}
      <QuoteSpecPickModal ctx={ctx} open={picking} onClose={() => setPicking(false)} name={name}
        onPick={rec => { const r = makeSupplierFinishRef(rec); write((r && r.name) || '', r); setPicking(false); }} />
    </Field>
  );
}

// The same catalog the Selection Hub, the door module and the quote line's own
// finish list read — vendor, then category, then a search over name and code.
function QuoteSpecPickModal({ ctx, open, onClose, name, onPick }) {
  const [pick, setPick] = useState('');
  const [q, setQ] = useState('');
  const groups = useMemo(() => supplierGroups(), []);
  const [sup, cat] = pick ? pick.split('\u0000') : ['', ''];
  const results = useMemo(() => (open && cat ? searchSupplierFinishes(sup, cat, q, 48) : []),
    [open, sup, cat, q]);
  return (
    <Modal open={open} onClose={onClose} size="lg" title={`${name} \u2014 pick from Supplier Finishes`}>
      <div className="flex flex-wrap gap-2 mb-3">
        <Select className="!w-72" value={pick} onChange={e => setPick(e.target.value)}>
          <option value="">Choose a supplier and category\u2026</option>
          {groups.map(g => (
            <optgroup key={g.key} label={supplierDisplayName(g.key, ctx.vendors)}>
              {g.cats.map(c => (
                <option key={c.sup + c.cat} value={`${c.sup}\u0000${c.cat}`}>{c.cat} ({c.count})</option>
              ))}
            </optgroup>
          ))}
        </Select>
        <TextInput className="!w-56" placeholder="Search name or code" value={q}
          onChange={e => setQ(e.target.value)} disabled={!cat} />
      </div>
      {!cat ? (
        <p className="text-xs text-[var(--leon-black)]/55">
          Pick a supplier and category to search. Anything not in the catalog can still be typed
          straight into the field.
        </p>
      ) : !results.length ? (
        <p className="text-xs text-[var(--leon-black)]/55">Nothing matches that search.</p>
      ) : (
        <div className="grid grid-cols-2 sm:grid-cols-4 gap-2 max-h-[52vh] overflow-y-auto">
          {results.map(rec => (
            <button key={rec.sup + rec.id} type="button" onClick={() => onPick(rec)}
              className="text-left rounded-lg border border-[var(--leon-line)] hover:border-[var(--leon-brown)] overflow-hidden bg-white">
              {rec.img
                ? <img src={rec.img} alt="" className="w-full h-16 object-cover" />
                : <div className="w-full h-16 bg-[var(--leon-cream)]" />}
              <div className="p-1.5">
                <div className="text-[11px] font-semibold leading-tight truncate" title={rec.name}>{rec.name}</div>
                <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{rec.code || rec.cat}</div>
              </div>
            </button>
          ))}
        </div>
      )}
    </Modal>
  );
}

function QuoteFinishModal({ ctx, open, onClose, line, set }) {
  const [pick, setPick] = useState('');
  const [q, setQ] = useState('');
  const groups = useMemo(() => supplierGroups(), []);
  const [sup, cat] = pick ? pick.split('\u0000') : ['', ''];
  const results = useMemo(() => (open && cat ? searchSupplierFinishes(sup, cat, q, 48) : []), [open, sup, cat, q]);
  const chosen = quoteLineFinishes(line);

  function add(rec) {
    const ref = makeSupplierFinishRef(rec);
    if (!ref) return;
    if (chosen.some(f => f.source === ref.source && f.id === ref.id)) return;
    set({ finishRefs: [...chosen, ref], finishRef: null });
  }
  function drop(f) {
    set({ finishRefs: chosen.filter(x => !(x.source === f.source && x.id === f.id)), finishRef: null });
  }

  return (
    <Modal open={open} onClose={onClose} wide
      title={`Proposed finishes${line.itemName ? ` \u2014 ${line.itemName}` : ''}`}
      footer={<Button onClick={onClose}>Done</Button>}>
      <div className="space-y-4">
        <div>
          <p className="text-[11px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1.5">On this line</p>
          {chosen.length ? (
            <div className="flex flex-wrap gap-2">
              {chosen.map(f => (
                <span key={f.source + ':' + f.id}
                  className="inline-flex items-center gap-2 rounded border border-[var(--leon-line)] bg-white pl-1 pr-2 py-1">
                  {f.img
                    ? <img src={f.img} alt="" className="w-8 h-8 rounded object-cover" />
                    : <span className="w-8 h-8 rounded bg-[var(--leon-cream)] border border-[var(--leon-line)]" />}
                  <span className="leading-tight">
                    <span className="block text-xs font-semibold">{f.name}</span>
                    <span className="block text-[10px] text-[var(--leon-black)]/45">
                      {f.code}{f.code && f.supLabel ? ' \u00b7 ' : ''}{f.supLabel || supplierDisplayName(f.source, ctx.vendors)}
                    </span>
                  </span>
                  <IconBtn title="Remove" onClick={() => drop(f)}>&#10005;</IconBtn>
                </span>
              ))}
            </div>
          ) : <p className="text-xs text-[var(--leon-black)]/45">Nothing chosen yet.</p>}
        </div>

        <div className="space-y-2">
          <p className="text-[11px] uppercase tracking-wide text-[var(--leon-black)]/45">Add from the catalogs</p>
          <div className="flex items-center gap-2">
            <Select value={pick} onChange={e => { setPick(e.target.value); setQ(''); }} className="!py-1 !text-xs !w-64">
              <option value="">Vendor &amp; construction…</option>
              {groups.map(g => (
                <optgroup key={g.key} label={supplierDisplayName(g.key, ctx.vendors)}>
                  {g.cats.map(c => <option key={c.sup + c.cat} value={`${c.sup}\u0000${c.cat}`}>{c.cat} ({c.count})</option>)}
                </optgroup>
              ))}
            </Select>
            <TextInput value={q} onChange={e => setQ(e.target.value)} disabled={!cat}
              placeholder={cat ? 'Search by name or supplier code\u2026' : 'Pick a vendor & category first'}
              className="!py-1 !text-xs flex-1" />
          </div>
          {cat && (
            results.length ? (
              <div className="grid grid-cols-3 sm:grid-cols-4 gap-2 max-h-72 overflow-y-auto pr-1">
                {results.map(r => {
                  const on = chosen.some(f => f.source === r.sup && f.id === r.id);
                  return (
                    <button key={r.id} type="button" onClick={() => add(r)}
                      className={`text-left rounded border p-1 hover:border-[var(--leon-brown)] ${on ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)]'}`}>
                      {r.img
                        ? <img src={r.img} alt="" loading="lazy" className="w-full h-16 object-cover rounded-sm" />
                        : <span className="block w-full h-16 rounded-sm bg-[var(--leon-cream)]" />}
                      <span className="block text-[10px] font-semibold truncate mt-1">{r.name}</span>
                      <span className="block text-[9px] text-[var(--leon-black)]/45 truncate">{r.code}</span>
                    </button>
                  );
                })}
              </div>
            ) : <p className="text-xs text-[var(--leon-black)]/45">Nothing matches.</p>
          )}
        </div>

        <Field label="Anything not in a catalog"
          hint="A client’s own specification, or a sample still being chased.">
          <TextInput value={line.proposedFinishes || ''} onChange={e => set({ proposedFinishes: e.target.value })}
            placeholder="Plywood core · lacquer" />
        </Field>
      </div>
    </Modal>
  );
}

function QuoteTariffPicker({ ctx, line, set, editable }) {
  const lib = ctx.tariffLibrary || [];
  const [q, setQ] = useState('');
  const countries = Array.from(new Set(lib.map(c => c.countryOfOrigin).filter(Boolean))).sort();
  const country = line.countryOfOrigin || '';
  const chosen = line.tariffClassificationId ? lib.find(c => c.id === line.tariffClassificationId) : null;

  const needle = q.trim().toLowerCase();
  const matches = !country && !needle ? [] : lib
    .filter(c => !country || c.countryOfOrigin === country)
    .filter(c => !needle
      || String(c.htsCode || c.hsCode || '').toLowerCase().includes(needle)
      || String(c.productDescription || '').toLowerCase().includes(needle))
    .slice(0, 25);

  function choose(c) {
    const v = currentTariffVersion(c);
    set({
      tariffClassificationId: c.id, countryOfOrigin: c.countryOfOrigin || line.countryOfOrigin || '',
      htsCode: c.htsCode || c.hsCode || '', dutyBasis: 'pct',
      dutyPct: v ? (Number(v.totalEstimatedDutyPct) || 0) / 100 : 0,
    });
    setQ('');
  }

  return (
    <div className="space-y-1.5">
      {chosen ? (
        <div className="flex items-center gap-2 text-[11px] rounded border border-[var(--leon-line)] bg-white px-2 py-1.5">
          <span className="font-semibold tabular-nums">{chosen.htsCode || chosen.hsCode}</span>
          <span className="text-[var(--leon-black)]/55">
            {chosen.countryOfOrigin} &rarr; {chosen.destinationCountry} ·{' '}
            {(() => { const v = currentTariffVersion(chosen); return v ? fmtPct(v.totalEstimatedDutyPct) : '\u2014'; })()}
          </span>
          <span className="text-[var(--leon-black)]/45 truncate">{chosen.productDescription}</span>
          {editable && (
            <button type="button" className="ml-auto text-[11px] font-semibold text-[var(--leon-brown)] hover:underline"
              onClick={() => set({ tariffClassificationId: null, dutyBasis: null, dutyPct: null, htsCode: '' })}>clear</button>
          )}
        </div>
      ) : (
        <>
          <div className="flex items-center gap-1.5">
            <Select className="!w-44 !text-xs !py-1" disabled={!editable} value={country}
              onChange={e => set({ countryOfOrigin: e.target.value })}>
              <option value="">Country of origin…</option>
              {countries.map(c => <option key={c} value={c}>{c}</option>)}
            </Select>
            <TextInput className="!text-xs !py-1 flex-1" disabled={!editable} value={q}
              placeholder="Code or description" onChange={e => setQ(e.target.value)} />
          </div>
          {!!matches.length && (
            <div className="max-h-40 overflow-y-auto rounded border border-[var(--leon-line)] bg-white divide-y divide-[var(--leon-line)]/60">
              {matches.map(c => {
                const v = currentTariffVersion(c);
                return (
                  <button key={c.id} type="button" disabled={!editable} onClick={() => choose(c)}
                    className="w-full text-left px-2 py-1 text-[11px] hover:bg-[var(--leon-cream)] flex items-center gap-2">
                    <span className="font-semibold tabular-nums whitespace-nowrap">{c.htsCode || c.hsCode}</span>
                    <span className="text-[var(--leon-brown)] whitespace-nowrap">{v ? fmtPct(v.totalEstimatedDutyPct) : '\u2014'}</span>
                    <span className="text-[var(--leon-black)]/55 truncate">{c.productDescription}</span>
                  </button>
                );
              })}
            </div>
          )}
          {(country || needle) && !matches.length && (
            <p className="text-[11px] text-[var(--leon-black)]/45">Nothing in the library matches.</p>
          )}
        </>
      )}
    </div>
  );
}

function QuoteLineWorking({ line, sec, qa, t, isLabor }) {
  const v = f => qpick(line[f], sec[f], qa[f]);
  const uom = line.uom || sec.uom || '';
  const driver = line.costDriver || sec.costDriver || 'qty';
  const driverWord = driver === 'module' ? 'modules' : driver === 'piece' ? 'pieces' : (uom || 'units');
  const rows = [];
  const push = (label, working, amount, muted) => rows.push({ label, working, amount, muted });

  const per = qnum(line.qtyPerItem), n = qnum(line.itemCount);
  push('Quantity',
    per && n ? `${qty4(per)} / item \u00d7 ${qty4(n)} item${n === 1 ? '' : 's'}` : `${qty4(t.qty)} entered`,
    null);

  if (isLabor) {
    push('Labour', `${qty4(t.qty)} ${uom} \u00d7 ${fmtMoney(qnum(line.laborUnit))}`, t.labor);
  } else {
    if (t.slabs) {
      const waste = qnum(qpick(line.slabWastePct, sec.slabWastePct));
      const yieldPer = qnum(qpick(line.slabYield, sec.slabYield));
      push('Slabs needed',
        `(${qty4(t.qty)} ${uom}${waste ? ` + ${pct(waste)} waste` : ''}) \u00f7 ${qty4(yieldPer)} per slab = ${qty4(t.slabs)}`, null);
      push('Slab material', `${qty4(t.slabs)} \u00d7 ${fmtMoney(qnum(qpick(line.slabRate, sec.slabRate)))}`,
        t.mat - t.c2s);
      if (t.c2s) push('Cut to size', `${qty4(t.qty)} ${uom} \u00d7 ${fmtMoney(qnum(line.matUnit))}`, t.c2s);
    } else if (driver === 'module' && per && n) {
      // The client's own chain, stated the way they write it: a rate per
      // module, a rate per type (that rate across one item's modules), then
      // the vendor cost across every item of that type.
      const rpm = qnum(line.matUnit);
      push('Rate per module', fmtMoney(rpm), null);
      push('Rate per type', `${fmtMoney(rpm)} \u00d7 ${qty4(per)} module${per === 1 ? '' : 's'} per item`, rpm * per);
      push('Vendor cost', `${fmtMoney(rpm * per)} per type \u00d7 ${qty4(n)} item${n === 1 ? '' : 's'}`, t.mat);
    } else {
      push('Material', `${qty4(t.qty)} ${uom} \u00d7 ${fmtMoney(qnum(line.matUnit))}`, t.mat);
    }
    const ohPct = qnum(qpick(line.overheadPct, sec.overheadPct, qa.overheadPct));
    if (t.overhead) push('Overhead', `${fmtMoney(t.mat)} material \u00d7 ${pct(ohPct)}`, t.overhead);

    if (t.containers) {
      const cap = qnum(v('containerCapacity'));
      // Freight is charged per MODULE, so what fills a container is modules in
      // one item times how many of that item — not the per-item figure, which
      // would size the shipment for a single apartment.
      const shipWorking = driver === 'module' && per && n
        ? `${qty4(per)} \u00d7 ${qty4(n)} = ${qty4(t.driverQty)} ${driverWord} \u00f7 ${qty4(cap)} per container = ${qty4(t.containers.containers)} container${t.containers.containers === 1 ? '' : 's'}`
        : `${qty4(t.driverQty)} ${driverWord} \u00f7 ${qty4(cap)} per container = ${qty4(t.containers.containers)} container${t.containers.containers === 1 ? '' : 's'}`;
      push('Container', shipWorking, null);
      if (t.containers.ocean) push('\u2003Ocean freight', `${qty4(t.containers.containers)} \u00d7 ${fmtMoney(qnum(v('freightPerContainer')))}`, t.containers.ocean, true);
      if (t.containers.inland) push('\u2003Inland', `${qty4(t.containers.containers)} \u00d7 ${fmtMoney(qnum(v('inlandPerContainer')))}`, t.containers.inland, true);
      if (t.containers.broker) push('\u2003Broker / clearance', `${qty4(t.containers.containers)} \u00d7 ${fmtMoney(qnum(v('brokerPerContainer')))}`, t.containers.broker, true);
      push('Freight', 'ocean + inland + broker', t.freight);
    } else if (t.freight) {
      push('Freight', quoteChargeLabel(line, sec, qa, 'freight'), t.freight);
    }
    if (t.duty) push('Duty', quoteChargeLabel(line, sec, qa, 'duty'), t.duty);
    if (t.install) push('Installation', `${qty4(t.qty)} ${uom} \u00d7 ${fmtMoney(qnum(line.installUnit))}`, t.install);
  }

  const method = quoteMethodFor(line, sec, qa);
  const rate = qnum(qpick(line.ratePct, sec.ratePct, qa.defaultRatePct));
  const sellWorking = (line.sellOverride !== null && line.sellOverride !== undefined && line.sellOverride !== '')
    ? 'set by hand'
    : method === 'unitPrice'
      ? `${qty4(t.qty)} ${uom} \u00d7 ${fmtMoney(qnum(qpick(line.unitPrice, sec.unitPrice)))} unit price`
      : quoteRateBasisFor(line, sec, qa) === 'markup'
        ? `${fmtMoney(t.cost)} cost + ${pct(rate)} markup`
        : `${fmtMoney(t.cost)} cost at ${pct(rate)} margin`;

  return (
    <div className="rounded border border-[var(--leon-line)] bg-white overflow-hidden">
      <div className="px-3 py-1.5 text-[10px] uppercase tracking-[0.12em] text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
        How this line was worked out
      </div>
      <table className="w-full text-[11px]">
        <tbody>
          {rows.map((r, i) => (
            <tr key={i} className={r.muted ? 'text-[var(--leon-black)]/55' : ''}>
              <td className="py-1 pl-3 pr-2 whitespace-nowrap w-40">{r.label}</td>
              <td className="py-1 pr-2 text-[var(--leon-black)]/60">{r.working}</td>
              <td className="py-1 pr-3 text-right tabular-nums whitespace-nowrap w-28">
                {r.amount === null ? '' : fmtMoney(r.amount)}
              </td>
            </tr>
          ))}
          <tr className="border-t border-[var(--leon-line)] font-semibold">
            <td className="py-1.5 pl-3 pr-2">Cost</td>
            <td className="py-1.5 pr-2 text-[var(--leon-black)]/50 font-normal">everything above</td>
            <td className="py-1.5 pr-3 text-right tabular-nums">{fmtMoney(t.cost)}</td>
          </tr>
          <tr className="font-semibold text-[var(--leon-brown)]">
            <td className="py-1 pl-3 pr-2">Sell</td>
            <td className="py-1 pr-2 text-[var(--leon-black)]/50 font-normal">{sellWorking}</td>
            <td className="py-1 pr-3 text-right tabular-nums">{fmtMoney(t.sell)}</td>
          </tr>
          {/* The three ways money leaves the sale, ONE ROW EACH — they are
              three different people's money (the salesperson, the GM, whoever
              referred the job) and rolling them into a single figure is how a
              referral fee stops being visible to anyone. */}
          {[['Less sales commission', t.commission, qpick(line.commissionPct, sec.commissionPct, qa.commissionPct), 'of the client price'],
            ['Less GM bonus', t.bonus, qpick(line.bonusPct, sec.bonusPct, qa.bonusPct), 'of the client price'],
            ['Less referral fee', t.referral, qpick(line.referralPct, sec.referralPct, qa.referralPct),
             qa.referralTo ? `to ${qa.referralTo}` : 'owed onward'],
           ].filter(r => r[1]).map(([label, amt, rt, note]) => (
            <tr key={label} className="text-[var(--leon-black)]/55">
              <td className="py-1 pl-3 pr-2">{label}</td>
              <td className="py-1 pr-2">{pct(qnum(rt))} {note}</td>
              <td className="py-1 pr-3 text-right tabular-nums">({fmtMoney(amt)})</td>
            </tr>
          ))}
          {!!qnum(t.markup) && (
            <tr className="text-[var(--leon-black)]/55">
              <td className="py-1 pl-3 pr-2">Less mark-up</td>
              <td className="py-1 pr-2">owed onward, not margin</td>
              <td className="py-1 pr-3 text-right tabular-nums">({fmtMoney(t.markup)})</td>
            </tr>
          )}
          <tr className="border-t border-[var(--leon-line)] font-semibold">
            <td className="py-1.5 pl-3 pr-2">Profit</td>
            <td className="py-1.5 pr-2 text-[var(--leon-black)]/50 font-normal">{pct(t.cm)} contribution margin</td>
            <td className="py-1.5 pr-3 text-right tabular-nums">{fmtMoney(t.profit)}</td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

function QuoteLineRow({ ctx, project, qa, sec, line, editable, showInstall, showLabor, showCharges, full, alt }) {
  const isLabor = sec.kind === 'labor';
  const isCombined = sec.kind === 'combined';
  const totalCols = 7
    + (isLabor ? 2 : (2 + (showCharges ? 4 : 0) + (showInstall ? 1 : 0) + (showLabor ? 1 : 0)))
    + (full ? 5 : 4) + 1;
  const [openDetail, setOpenDetail] = useState(false);
  const t = quoteLineTotals(line, qa, sec);
  const set = f => ctx.updateQuoteLine(project.id, qa.id, sec.id, line.id, f);
  // null on the line means "however this scope is priced" — shown as Scope, so
  // the inherited answer is visible rather than assumed.
  const method = line.pricingMethod || null;
  const effective = quoteMethodFor(line, sec, qa);
  const vendors = (ctx.vendors || []).filter(v => v.active !== false);

  // ── An area heading, or a note ────────────────────────────────────────────
  // Neither is a priced line, so neither gets the twenty columns. A heading
  // reads as a heading — full width, ruled, the area named — and a note reads
  // as a sentence. They sit in `lines`, so they stay where they were put and a
  // heading keeps the lines it introduces underneath it.
  if (!quoteRowIsItem(line)) {
    const isArea = line.rowKind === 'area';
    const remove = () => {
      if (confirm(`Remove this ${isArea ? 'area heading' : 'note'}?`)) ctx.removeQuoteLine(project.id, qa.id, sec.id, line.id);
    };
    return (
      <tr className={isArea
        ? 'border-t-2 border-b border-[var(--leon-brown)]/35 bg-[var(--leon-cream)]'
        : 'border-b border-[var(--leon-line)]/60'}>
        <td colSpan={totalCols} className={isArea ? 'py-1.5 pr-2' : 'py-1 pr-2'}>
          <div className="flex items-center gap-2">
            <span className="text-[10px] text-[var(--leon-black)]/30 shrink-0" aria-hidden="true">
              {isArea ? '\u25A6' : '\u270E'}
            </span>
            <input value={line.description || ''} disabled={!editable}
              placeholder={isArea ? 'Which area these lines are for — e.g. Level 4, Units 401–412'
                                  : 'A note about the lines around this'}
              onChange={e => set({ description: e.target.value })}
              className={`q-cell flex-1 ${isArea
                ? 'font-bold uppercase tracking-[0.1em] text-[11px]'
                : 'italic text-[var(--leon-black)]/65'}`} />
            {editable && (
              <button onClick={remove} title={`Remove this ${isArea ? 'area heading' : 'note'}`}
                className="text-[var(--leon-black)]/25 hover:text-[var(--leon-red)] px-1 shrink-0">&#10005;</button>
            )}
          </div>
          {/* The specification for everything under this heading — the same
              panel the wizard shows, so a scope reads the same either way. */}
          {isArea && (
            <div className="mt-1.5">
              <QuoteAreaSpec ctx={ctx} sec={sec} area={line} set={set} editable={editable} />
            </div>
          )}
        </td>
      </tr>
    );
  }

  return (
    <>
      <tr className={`border-b border-[var(--leon-line)]/60 align-middle ${alt ? 'q-alt' : ''} ${line.excluded ? 'opacity-45' : ''}`}>
        <td className="py-1 pr-2">
          <input value={line.itemName || line.itemTag || ''} disabled={!editable} placeholder="Unit 1"
            onChange={e => set({ itemName: e.target.value })}
            className="q-cell w-24 font-semibold" />
        </td>
        <td className="py-1 pr-2">
          <input value={line.itemType || ''} disabled={!editable} placeholder="Kitchen"
            onChange={e => set({ itemType: e.target.value })}
            className="q-cell w-24" />
        </td>
        <td className="py-1 pr-2">
          <input value={line.description || ''} disabled={!editable}
            onChange={e => set({ description: e.target.value })}
            className="q-cell w-full" />
        </td>
        {/* What is being offered. The finish reference wins when one is picked —
            it points at a real catalogue record — and the text is what you write
            before there is one. */}
        <td className="py-1 pr-2">
          <QuoteFinishCell ctx={ctx} line={line} set={set} editable={editable} />
        </td>
        {/* The two quantities, and their product. Keeping them apart is what
            makes "five identical kitchens of seventeen modules" readable
            instead of "85". */}
        <td className="py-1 pr-2 text-right"><QNum w="w-16" value={line.qtyPerItem} disabled={!editable} onChange={v => editable && set({ qtyPerItem: v })} /></td>
        <td className="py-1 pr-2 text-right"><QNum w="w-12" value={line.itemCount} disabled={!editable} onChange={v => editable && set({ itemCount: v })} placeholder="1" /></td>
        <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap font-semibold">
          {qty4(t.qty)}
          <span className="text-[10px] text-[var(--leon-black)]/40 ml-1">{line.uom || sec.uom}</span>
        </td>
        <td className="py-1 pr-2 text-right border-l border-[var(--leon-line)]">
          {isLabor
            ? <QNum w="w-20" prefix="$" value={line.laborUnit} disabled={!editable} onChange={v => editable && set({ laborUnit: v })} />
            : <QNum w="w-20" prefix="$" value={line.matUnit} disabled={!editable} onChange={v => editable && set({ matUnit: v })} />}
        </td>
        <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap">
          {isLabor ? (t.labor ? fmtMoney(t.labor) : '—') : (t.mat ? fmtMoney(t.mat) : '—')}
        </td>
        {showCharges && <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap text-[var(--leon-black)]/60">{t.overhead ? fmtMoney(t.overhead) : '—'}</td>}
        {/* The RATE. Where freight is charged per unit it is the number itself
            and editable here; on every other basis it is DERIVED — total over
            quantity — so the column answers "what is shipping costing me per
            unit" the same way whatever basis the scope uses. A derived figure
            is shown in italic and cannot be typed into, because the way to
            change it is to change the basis. */}
        {showCharges && (
          <td className="py-1 pr-2 text-right whitespace-nowrap">
            {quoteFreightBasisFor(line, sec, qa) === 'perUnit'
              ? <QNum w="w-20" prefix="$" value={line.freightPerUnit}
                  disabled={!editable} onChange={v => editable && set({ freightPerUnit: v })} />
              : <span className="tabular-nums italic text-[var(--leon-black)]/50"
                  title={`Derived — ${quoteChargeLabel(line, sec, qa, 'freight')}`}>
                  {t.qty && t.freight ? fmtMoney(t.freight / t.qty) : '—'}
                </span>}
          </td>
        )}
        {showCharges && (
          <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap text-[var(--leon-black)]/60">
            {t.freight ? fmtMoney(t.freight) : '—'}
            <div className="text-[9px] text-[var(--leon-black)]/40 leading-tight">
              {t.containers ? `${qty4(t.containers.containers)} ctr` : quoteChargeLabel(line, sec, qa, 'freight')}
            </div>
          </td>
        )}
        {showCharges && (
          <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap text-[var(--leon-black)]/60">
            {t.duty ? fmtMoney(t.duty) : '—'}
            <div className="text-[9px] text-[var(--leon-black)]/40 leading-tight">{quoteChargeLabel(line, sec, qa, 'duty')}</div>
          </td>
        )}
        {showLabor && (
          <td className="py-1 pr-2 text-right">
            <QNum w="w-20" prefix="$" value={line.laborUnit} disabled={!editable} onChange={v => editable && set({ laborUnit: v })} />
            <div className="text-[9px] text-[var(--leon-black)]/40 leading-tight tabular-nums">{t.labor ? fmtMoney(t.labor) : ''}</div>
          </td>
        )}
        {showInstall && (
          <td className="py-1 pr-2 text-right">
            <QNum w="w-20" prefix="$" value={line.installUnit} disabled={!editable} onChange={v => editable && set({ installUnit: v })} />
            <div className="text-[9px] text-[var(--leon-black)]/40 leading-tight tabular-nums">{t.install ? fmtMoney(t.install) : ''}</div>
          </td>
        )}
        <td className="py-1 pr-2 text-right tabular-nums whitespace-nowrap font-semibold border-l border-[var(--leon-line)]">{fmtMoney(t.cost)}</td>
        {/* Stacked, this cell was two controls tall and made every row ragged.
            Side by side it stays on one line: how the line is priced, then the
            number, with the scope's own answer as the placeholder so an
            inherited value is visible without being typed. */}
        <td className="py-1 pr-2">
          <div className="flex items-center justify-end gap-1">
            <select value={method || ''} disabled={!editable} onChange={e => set({ pricingMethod: e.target.value || null })}
              title={effective === 'unitPrice' ? 'Priced at a set rate per unit' : 'Priced at cost plus a margin'}
              className="w-[68px] px-1 py-0.5 text-[10px] border border-[var(--leon-line)] rounded bg-white text-[var(--leon-black)]/70">
              <option value="">Scope</option>
              {QUOTE_PRICING_METHODS.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
            </select>
            {effective === 'unitPrice'
              ? <QNum w="w-16" prefix="$" placeholder={sec.unitPrice != null ? String(sec.unitPrice) : ''}
                  value={line.unitPrice} disabled={!editable} onChange={v => editable && set({ unitPrice: v })} />
              : <QPct w="w-12" placeholder={String(Math.round(qnum(sec.ratePct != null ? sec.ratePct : qa.defaultRatePct) * 100))}
                  value={line.ratePct} disabled={!editable} onChange={v => editable && set({ ratePct: v })} />}
          </div>
        </td>
        <td className="py-1 pr-2 text-right tabular-nums font-semibold whitespace-nowrap">
          {fmtMoney(t.sell)}
          {line.sellOverride !== null && line.sellOverride !== undefined && line.sellOverride !== '' &&
            <span className="ml-1 text-[9px] uppercase text-[var(--leon-brown)]" title="Sell price set by hand">fixed</span>}
        </td>
        {full && (
          <td className="py-1 pr-2 text-right tabular-nums font-semibold whitespace-nowrap">
            {t.sell ? fmtMoney(t.profit) : <span className="opacity-40">—</span>}
            {!!(t.commission + t.bonus) && (
              <div className="text-[9px] text-[var(--leon-black)]/40 leading-tight">after {fmtMoney(t.commission + t.bonus)} comm</div>
            )}
          </td>
        )}
        <td className={`py-1 pr-2 text-right tabular-nums ${t.cm < 0.18 && t.sell ? 'text-red-600 font-semibold' : ''}`}>
          {t.sell ? pct(t.cm) : <span className="opacity-40">—</span>}
        </td>
        <td className="py-1 text-right">
          <button onClick={() => setOpenDetail(!openDetail)} title="Line detail"
            className="text-[var(--leon-black)]/35 hover:text-[var(--leon-brown)] px-1">⋯</button>
        </td>
      </tr>
      {openDetail && (
        <tr className="q-detail bg-[var(--leon-cream)]/60 border-b border-[var(--leon-line)]">
          <td colSpan={totalCols} className="p-3 space-y-3">
            <QuoteLineWorking line={line} sec={sec} qa={qa} t={t} isLabor={isLabor} />
            {/* WHAT IS BEING BOUGHT, in the words this trade uses. Every scope
                asks for six different things and the lists already exist on
                TAKEOFF_SCOPES — a countertop wants thickness and edge profile,
                a door wants core and fire rating, carpet wants backing and
                pattern repeat. Reading them from the same place the take-off
                reads them means the quote and the take-off cannot end up asking
                two different questions about one product. */}
            {(() => {
              const fields = quoteSpecFieldsFor(sec.scopeKey || sec.name);
              if (!fields.length) return null;
              const specs = line.specs || {};
              return (
                <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 mb-3">
                  <p className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 font-bold mb-2">
                    {(sec.scopeKey || sec.name || 'Scope').replace(/ — .*$/, '')} specification
                  </p>
                  <div className="grid gap-2 md:grid-cols-3">
                    {/* Room code and Area are on every trade's list and are
                        already fields on the line — they are shown here, at the
                        head of the specification, rather than stored a second
                        time inside `specs`. Two copies of a room number is how
                        they come to disagree. */}
                    <Field key="__room" label="Room code">
                      <TextInput value={line.location || ''} disabled={!editable}
                        onChange={e => set({ location: e.target.value })} placeholder="e.g. 4B-KIT" />
                    </Field>
                    <Field key="__area" label="Area">
                      <TextInput value={line.area || ''} disabled={!editable}
                        onChange={e => set({ area: e.target.value })} placeholder="e.g. Kitchen" />
                    </Field>
                  </div>
                  {/* The specification is answered ONCE on the area heading, so
                      it is READ here rather than asked again. Repeating nine
                      fields under every line is the typing that moving it to the
                      area was meant to stop — but a line still has to show what
                      it is being quoted on, so it is stated. */}
                  <QuoteLineSpecSummary sec={sec} line={line} fields={fields} />
                  {/* The client's own chain, stated as the arithmetic that
                      produced it. Both figures are DERIVED — the editable
                      numbers are the module count, the type quantity and the
                      module rate above; a total you can type over is a total
                      that stops agreeing with its own inputs. */}
                  {(sec.costDriver || 'qty') === 'module' && (
                    <div className="mt-3 pt-2 border-t border-[var(--leon-line)] text-[11px] tabular-nums flex flex-wrap gap-x-5 gap-y-1">
                      <span className="text-[var(--leon-black)]/55">
                        Type rate <span className="text-[var(--leon-black)]/35">
                          ({qnum(line.qtyPerItem).toLocaleString()} modules &times; {fmtMoney(qnum(line.matUnit))})
                        </span>{' '}
                        <strong className="text-[var(--leon-black)]">
                          {fmtMoney(qnum(line.qtyPerItem) * qnum(line.matUnit))}
                        </strong>
                      </span>
                      <span className="text-[var(--leon-black)]/55">
                        Vendor cost <span className="text-[var(--leon-black)]/35">
                          (type rate &times; {qnum(line.itemCount).toLocaleString()} types)
                        </span>{' '}
                        <strong className="text-[var(--leon-black)]">
                          {fmtMoney(qnum(line.qtyPerItem) * qnum(line.matUnit) * qnum(line.itemCount))}
                        </strong>
                      </span>
                    </div>
                  )}
                  <p className="text-[10px] text-[var(--leon-black)]/40 mt-1.5">
                    These travel to the client quote alongside the selection.
                  </p>
                </div>
              );
            })()}

            <div className="grid gap-3 md:grid-cols-4">
              <Field label="Vendor">
                <Select value={line.vendorId || ''} disabled={!editable}
                  onChange={e => set({ vendorId: e.target.value || null })}>
                  <option value="">—</option>
                  {vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
                </Select>
              </Field>
              {/* Only the rates this kind of scope actually has: a Supply Only
                  scope is not installed by us, so an install rate on it is a
                  field that can only ever be wrong. */}
              {!isLabor && sec.kind !== 'supply' && (
                <Field label="Install / unit"><QNum w="w-24" prefix="$" value={line.installUnit} disabled={!editable} onChange={v => editable && set({ installUnit: v })} /></Field>
              )}
              {isCombined && (
                <Field label="Labour / unit"><QNum w="w-24" prefix="$" value={line.laborUnit} disabled={!editable} onChange={v => editable && set({ laborUnit: v })} /></Field>
              )}
              <Field label="Area"><TextInput value={line.area || ''} disabled={!editable} onChange={e => set({ area: e.target.value })} /></Field>
              <Field label="Category"><TextInput value={line.category || ''} disabled={!editable} onChange={e => set({ category: e.target.value })} /></Field>
              <Field label="Location / room"><TextInput value={line.location || ''} disabled={!editable} onChange={e => set({ location: e.target.value })} /></Field>
              <Field label="UoM"><TextInput value={line.uom || ''} disabled={!editable} onChange={e => set({ uom: e.target.value })} /></Field>
              <div className="md:col-span-2"><QuoteChargeEditor which="freight" obj={line} parent={sec} qa={qa}
                onChange={set} editable={editable} canInherit showCbm /></div>
              <div className="md:col-span-2"><QuoteChargeEditor which="duty" obj={line} parent={sec} qa={qa}
                onChange={set} editable={editable} canInherit showCbm /></div>
              {/* The tariff can be the scope's or this item's — the charge
                  editor above already inherits — and where it is this item's,
                  it should come from the classification rather than a typed
                  guess. Picking a code sets the rate from the library that the
                  Export and Logistics screens read. */}
              <Field label="HTS classification" className="md:col-span-2"
                hint={line.dutyPct === null || line.dutyPct === undefined
                  ? 'This item follows the scope\u2019s tariff. Pick a code to give it its own.'
                  : 'This item has its own tariff rate.'}>
                <QuoteTariffPicker ctx={ctx} line={line} set={set} editable={editable} />
              </Field>
              <Field label="Sell price override" hint="A rounded number agreed with the client. Wins over everything.">
                <QNum w="w-28" prefix="$" value={line.sellOverride} disabled={!editable} onChange={v => editable && set({ sellOverride: v })} /></Field>
              <Field label="Not in this price">
                <label className="flex items-center gap-2 text-xs pt-1.5">
                  <input type="checkbox" checked={!!line.excluded} disabled={!editable} onChange={e => set({ excluded: e.target.checked })} />
                  <span>Carry the line, exclude the money</span>
                </label>
              </Field>
              <Field label="Note" className="md:col-span-3">
                <TextInput value={line.note || ''} disabled={!editable} onChange={e => set({ note: e.target.value })} /></Field>
              <div className="flex items-end">
                {editable && <button onClick={() => ctx.removeQuoteLine(project.id, qa.id, sec.id, line.id)}
                  className="text-xs font-semibold text-red-600">Remove line</button>}
              </div>
            </div>
            {(Object.keys(line.specs || {}).length > 0 || line.drawingRef || line.takeoffStatus || line.unitType) && (
              <div className="mt-3 pt-3 border-t border-[var(--leon-line)] text-[11px] text-[var(--leon-black)]/60">
                <span className="font-semibold uppercase tracking-wide mr-2">From the take-off</span>
                {line.unitType && <span className="mr-3">{line.unitType} × {line.unitQty} @ {line.qtyPerUnit} + {pct(line.wastePct)} waste</span>}
                {line.drawingRef && <span className="mr-3">Dwg {line.drawingRef}</span>}
                {line.takeoffStatus && <span className="mr-3">{line.takeoffStatus}</span>}
                {Object.entries(line.specs || {}).map(([k, v]) => <span key={k} className="mr-3">{k}: <strong>{v}</strong></span>)}
              </div>
            )}
          </td>
        </tr>
      )}
    </>
  );
}

// Job Costs — who this job is paying, and every invoice behind it, in the one
// place the job's salesperson can reach. It also carries the sales approval
// step: a subcontractor invoice sits at "Pending Sales Approval" until the
// person who sold the job confirms the work was actually done to the scope
// they sold. Before this the button existed but lived on a screen sales could
// not open, so only Admin and the GM could ever press it.
function SalesJobCostsSubTab({ ctx, project }) {
  // Clicking a cost line opens that invoice, rather than naming a record and
  // leaving you to go find it in the Financial Hub.
  const [openInvoice, setOpenInvoice] = useState(null);
  const [rejecting, setRejecting] = useState(null);
  const [reason, setReason] = useState('');
  const [q, setQ] = useState('');
  const [fType, setFType] = useState('all');
  const [fApproval, setFApproval] = useState('all');
  const [fPay, setFPay] = useState('all');
  const [onlyMine, setOnlyMine] = useState(false);
  const all = liveApInvoices(project);
  const ql = q.trim().toLowerCase();
  const invoices = all.filter(i =>
    (fType === 'all' || (i.partyType || 'Vendor') === fType) &&
    (fApproval === 'all' || i.approvalStatus === fApproval) &&
    (fPay === 'all' || i.paymentStatus === fPay) &&
    (!onlyMine || ctx.canSalesApprove(i)) &&
    (!ql || [i.vendorName, i.invoiceNumber, i.description].some(v => (v || '').toLowerCase().includes(ql)))
  );
  const byType = t => invoices.filter(i => (i.partyType || 'Vendor') === t);
  const subs = byType('Subcontractor');
  const others = invoices.filter(i => (i.partyType || 'Vendor') !== 'Subcontractor');
  // The banner counts across the WHOLE job, never the filtered view — hiding
  // an approval behind a filter is exactly how one gets forgotten.
  const pending = all.filter(i => ctx.canSalesApprove(i));

  // Every party this job owes money to, whichever record it came through.
  const parties = {};
  function note(name, type, amount, paid) {
    if (!name) return;
    const k = type + '|' + name;
    if (!parties[k]) parties[k] = { name, type, invoiced: 0, paid: 0, count: 0 };
    parties[k].invoiced += amount; parties[k].paid += paid; parties[k].count += 1;
  }
  invoices.forEach(inv => {
    const paid = (inv.payments || []).reduce((n, x) => n + (Number(x.amount) || 0), 0);
    note(inv.vendorName, inv.partyType || 'Vendor', Number(inv.amount) || 0, paid);
  });
  if (invoices.length === all.length) {
    (project.vendorEstimates || []).forEach(e => { if (!Object.keys(parties).some(k => k.endsWith('|' + e.vendorName))) note(e.vendorName, 'Vendor', 0, 0); });
  }
  const partyList = Object.values(parties).sort((a, b) => b.invoiced - a.invoiced);
  const totalInvoiced = invoices.reduce((n, i) => n + (Number(i.amount) || 0), 0);
  const totalPaid = invoices.reduce((n, i) => n + (i.payments || []).reduce((m, x) => m + (Number(x.amount) || 0), 0), 0);

  function row(inv) {
    const paid = (inv.payments || []).reduce((n, x) => n + (Number(x.amount) || 0), 0);
    const open = (Number(inv.amount) || 0) - paid;
    const mine = ctx.canSalesApprove(inv);
    return (
      <tr key={inv.id} onClick={() => setOpenInvoice(inv.id)} title="Open this invoice"
        className={`border-t border-[var(--leon-line)] cursor-pointer hover:bg-[var(--leon-cream)] ${mine ? 'bg-[var(--leon-yellow)]/10' : ''}`}>
        <td className="px-3 py-2 font-semibold text-[var(--leon-brown)] hover:underline">{inv.vendorName || '—'}</td>
        <td className="px-3 py-2">{inv.invoiceNumber}</td>
        <td className="px-3 py-2">{inv.partyType || 'Vendor'}</td>
        <td className="px-3 py-2 whitespace-nowrap">{fmtDate(inv.invoiceDate)}</td>
        <td className="px-3 py-2 whitespace-nowrap">{fmtDate(inv.dueDate)}</td>
        <td className="px-3 py-2 text-right font-semibold tabular-nums">{fmtMoney(inv.amount)}</td>
        <td className="px-3 py-2 text-right tabular-nums">{fmtMoney(open)}</td>
        <td className="px-3 py-2"><StatusBadge status={inv.approvalStatus} /></td>
        <td className="px-3 py-2 whitespace-nowrap" onClick={e => e.stopPropagation()}>
          {mine && (
            <>
              <Button size="sm" onClick={() => ctx.salesApproveApInvoice(project.id, inv.id, true)}>Approve</Button>
              <Button size="sm" variant="ghost" onClick={() => { setRejecting(inv); setReason(''); }}>Send back</Button>
            </>
          )}
        </td>
      </tr>
    );
  }

  const head = (
    <thead>
      <tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50 bg-[var(--leon-cream)]">
        <th className="px-3 py-2">Party</th><th className="px-3 py-2">Invoice #</th><th className="px-3 py-2">Type</th>
        <th className="px-3 py-2">Invoiced</th><th className="px-3 py-2">Due</th>
        <th className="px-3 py-2 text-right">Amount</th><th className="px-3 py-2 text-right">Open</th>
        <th className="px-3 py-2">Approval</th><th className="px-3 py-2"></th>
      </tr>
    </thead>
  );

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3 max-w-3xl">
        Everyone this job is paying, and every invoice behind it. As the salesperson on this job
        you release the subcontractor invoices &mdash; confirm the work was actually done to the
        scope you sold, then approve. Nothing gets paid until you do.
      </p>

      {pending.length > 0 && (
        <div className="mb-4 px-3 py-2 rounded-lg border border-[var(--leon-yellow)] bg-[var(--leon-yellow)]/10">
          <p className="text-sm font-bold text-[var(--leon-yellow)]">
            &#9888; {pending.length} invoice{pending.length === 1 ? '' : 's'} waiting on your approval
          </p>
          <p className="text-xs text-[var(--leon-black)]/60">
            {pending.map(i => `${i.vendorName || i.invoiceNumber} (${fmtMoney(i.amount)})`).join(', ')}
          </p>
        </div>
      )}

      <div className="grid sm:grid-cols-3 gap-3 mb-3">
        <StatBox label={invoices.length === all.length ? 'Total Invoiced to This Job' : 'Invoiced (filtered)'} value={fmtMoney(totalInvoiced)} />
        <StatBox label="Paid to Date" value={fmtMoney(totalPaid)} />
        <StatBox label="Still Open" value={fmtMoney(totalInvoiced - totalPaid)} />
      </div>

      {/* Filters act on every list below AND on the totals above, so what you
          are looking at and what it adds up to always agree. */}
      <div className="flex items-center gap-2 flex-wrap mb-4">
        <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search party, invoice # or description…" className="!w-60 !py-1 !text-xs" />
        <Select value={fType} onChange={e => setFType(e.target.value)} className="!w-44 !py-1 !text-xs">
          <option value="all">All parties</option>
          {[...new Set(all.map(i => i.partyType || 'Vendor'))].sort().map(t => <option key={t} value={t}>{t}</option>)}
        </Select>
        <Select value={fApproval} onChange={e => setFApproval(e.target.value)} className="!w-52 !py-1 !text-xs">
          <option value="all">Any approval status</option>
          {[...new Set(all.map(i => i.approvalStatus).filter(Boolean))].sort().map(t => <option key={t} value={t}>{t}</option>)}
        </Select>
        <Select value={fPay} onChange={e => setFPay(e.target.value)} className="!w-44 !py-1 !text-xs">
          <option value="all">Any payment status</option>
          {[...new Set(all.map(i => i.paymentStatus).filter(Boolean))].sort().map(t => <option key={t} value={t}>{t}</option>)}
        </Select>
        {pending.length > 0 && (
          <label className="flex items-center gap-1.5 text-xs font-semibold text-[var(--leon-yellow)] cursor-pointer">
            <input type="checkbox" checked={onlyMine} onChange={e => setOnlyMine(e.target.checked)} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
            Waiting on me ({pending.length})
          </label>
        )}
        <div className="flex-1" />
        <span className="text-xs text-[var(--leon-black)]/45">{invoices.length} of {all.length}</span>
        {(ql || fType !== 'all' || fApproval !== 'all' || fPay !== 'all' || onlyMine) && (
          <button onClick={() => { setQ(''); setFType('all'); setFApproval('all'); setFPay('all'); setOnlyMine(false); }}
            className="text-xs font-semibold text-[var(--leon-brown)] hover:underline">Clear</button>
        )}
      </div>

      <Collapsible title="Vendors &amp; subcontractors on this job" count={partyList.length} defaultOpen>
        {partyList.length === 0 ? <EmptyState text="Nobody has invoiced this job yet." /> : (
          <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
            <table className="w-full text-xs">
              <thead><tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50 bg-[var(--leon-cream)]">
                <th className="px-3 py-2">Party</th><th className="px-3 py-2">Type</th><th className="px-3 py-2 text-right">Invoices</th>
                <th className="px-3 py-2 text-right">Invoiced</th><th className="px-3 py-2 text-right">Paid</th><th className="px-3 py-2 text-right">Open</th>
              </tr></thead>
              <tbody>
                {partyList.map(v => (
                  <tr key={v.type + v.name} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-2 font-semibold">{v.name}</td>
                    <td className="px-3 py-2">{v.type}</td>
                    <td className="px-3 py-2 text-right tabular-nums">{v.count}</td>
                    <td className="px-3 py-2 text-right font-semibold tabular-nums">{fmtMoney(v.invoiced)}</td>
                    <td className="px-3 py-2 text-right tabular-nums">{fmtMoney(v.paid)}</td>
                    <td className="px-3 py-2 text-right tabular-nums">{fmtMoney(v.invoiced - v.paid)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </Collapsible>

      <Collapsible title="Subcontractor invoices" count={subs.length} defaultOpen>
        {subs.length === 0 ? <EmptyState text="No subcontractor invoices on this job." /> : (
          <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
            <table className="w-full text-xs" style={{ minWidth: 860 }}>{head}<tbody>{subs.map(row)}</tbody></table>
          </div>
        )}
      </Collapsible>

      <Collapsible title="Other cost invoices" count={others.length}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">Vendor, freight and miscellaneous invoices charged to this job.</p>
        {others.length === 0 ? <EmptyState text="No other cost invoices on this job." /> : (
          <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
            <table className="w-full text-xs" style={{ minWidth: 860 }}>{head}<tbody>{others.map(row)}</tbody></table>
          </div>
        )}
      </Collapsible>

      <Modal open={!!rejecting} onClose={() => setRejecting(null)} title="Send this invoice back"
        footer={<><Button variant="ghost" onClick={() => setRejecting(null)}>Cancel</Button>
                 <Button variant="danger" disabled={!reason.trim()}
                   onClick={() => { ctx.salesApproveApInvoice(project.id, rejecting.id, false, reason.trim()); setRejecting(null); }}>Send back</Button></>}>
        {rejecting && (
          <div className="space-y-3">
            <p className="text-sm text-[var(--leon-black)]/60">
              <b>{rejecting.vendorName}</b> &mdash; {rejecting.invoiceNumber} &mdash; {fmtMoney(rejecting.amount)}
            </p>
            <Field label="Why is it going back?" hint="The subcontractor and Accounting both see this">
              <TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} placeholder="e.g. unit 12B was not completed on this visit" />
            </Field>
          </div>
        )}
      </Modal>
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}
// A single, at-a-glance rollup of everything in the Sales hub — contract
// value, quote history, profitability, and change orders/back charges —
// "see all together" rather than clicking through each sub-tab.
function SalesOverviewSubTab({ ctx, project }) {
  const rcv = revisedContractValue(project);
  const latestQuote = [...project.quoteRevisions].sort((a, b) => b.revision - a.revision)[0];
  const approvedCOs = project.changeOrders.filter(c => c.type === 'Change Order' && c.status === 'Approved');
  const approvedBCs = project.changeOrders.filter(c => c.type === 'Back Charge' && c.status === 'Approved');
  const pendingCount = project.changeOrders.filter(c => c.status === 'Pending').length;
  const summary = projectProfitabilitySummary(project);
  return (
    <div>
      <div className="grid sm:grid-cols-2 gap-3 mb-3">
        <div className="border border-[var(--leon-line)] rounded-lg p-3">
          <p className="text-xs text-[var(--leon-black)]/50 uppercase font-semibold">Original Contract Value</p>
          <p className="text-xl font-bold">{fmtMoney(project.originalContractValue)}</p>
        </div>
        <div className="border-2 border-[var(--leon-brown)] rounded-lg p-3 bg-[var(--leon-cream)]">
          <p className="text-xs text-[var(--leon-brown)] uppercase font-semibold">Revised Contract Value</p>
          <p className="text-xl font-bold">{fmtMoney(rcv)}</p>
        </div>
      </div>
      <div className="grid sm:grid-cols-4 gap-3 mb-5">
        <StatBox label="Latest Quote" value={latestQuote ? `R${latestQuote.revision} — ${fmtMoney(latestQuote.amount)}` : '—'} />
        <StatBox label="Approved Change Orders" value={`${approvedCOs.length} (${fmtMoney(approvedCOs.reduce((s, c) => s + c.amount, 0))})`} />
        <StatBox label="Approved Back-charges" value={`${approvedBCs.length} (${fmtMoney(approvedBCs.reduce((s, c) => s + c.amount, 0))})`} tone={approvedBCs.length ? 'red' : undefined} />
        <StatBox label="Pending Approval" value={String(pendingCount)} tone={pendingCount ? 'yellow' : undefined} />
      </div>
      <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">Projected Profitability (by scope)</p>
      <div className="overflow-x-auto mb-2">
        <table className="w-full text-xs">
          <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3">Scope</th><th className="py-1 pr-3">Sales Value</th><th className="py-1 pr-3">Projected Profit</th><th className="py-1 pr-3">Projected Margin %</th></tr></thead>
          <tbody>
            {summary.rows.map(r => (
              <tr key={r.scope.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 pr-3 font-semibold">{r.scope.name}</td>
                <td className="py-1.5 pr-3">{fmtMoney(r.scope.profitability.salesValue)}</td>
                <td className="py-1.5 pr-3">{fmtMoney(r.projectedProfit)}</td>
                <td className="py-1.5 pr-3">{fmtPct(r.projectedMarginPct)}</td>
              </tr>
            ))}
            <tr className="border-t-2 border-[var(--leon-black)] font-bold">
              <td className="py-1.5 pr-3">Total</td>
              <td className="py-1.5 pr-3">{fmtMoney(summary.totalSales)}</td>
              <td className="py-1.5 pr-3">{fmtMoney(summary.totalProfit)}</td>
              <td className="py-1.5 pr-3">{fmtPct(summary.overallMarginPct)}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/40">See the Quotes, Profitability, Change Orders, and Back-charges tabs for full detail.</p>
    </div>
  );
}
// One QUOTATION. Everything client-facing lives here now: the deck's slides,
// its pictures, its lead times, the read-only view, the editable copy, and —
// once submitted — sending it. The analysis that priced it is a link away, not
// a tab of this, because they answer different questions for different readers.
function QuoteRevisionDetail({ ctx, project, quote, editable, onBack, onConvert }) {
  const qa = (project.quoteAnalyses || []).find(a => a.id === quote.quoteAnalysisId) || null;
  const drafting = (quote.stage || 'submitted') === 'drafting';
  const canEdit = editable && drafting;

  // The client document is built from the ANALYSIS this quotation came from —
  // through the same whitelist as before, so nothing outside it can reach a
  // slide. A quotation with no analysis behind it (one typed in by hand) has no
  // deck to build, and says so rather than rendering an empty one.
  const doc = useMemo(() => {
    if (!qa) return null;
    try {
      return clientQuoteFromAnalysis(qa, { detail: 'line',
        specImageFields: (() => {
          const acc = {};
          (qa.sections || []).forEach(sec => {
            const k = sec.scopeKey || sec.name;
            acc[k] = quoteSpecImageFieldsFor(k, ctx.quoteSpecImages, quoteSpecFieldsFor(k));
          });
          return acc;
        })() });
    } catch (e) { return null; }
  }, [qa, ctx.quoteSpecImages]);
  const leaks = useMemo(() => (doc && typeof clientQuoteLeaks === 'function') ? clientQuoteLeaks(doc) : [], [doc]);

  // The deck's arrangement lives on the QUOTATION, not the analysis — this is
  // the client document, and the analysis may be revised behind it.
  const deckQa = useMemo(() => qa ? Object.assign({}, qa, {
    id: quote.id,
    deckSlides: quote.deckSlides || null,
    deckPictures: quote.deckPictures || {},
    extraSpecs: quote.extraSpecs || {},
    leadTimes: quote.leadTimes || null,
    revision: quote.revision,
    status: drafting ? 'Draft' : 'Issued',
  }) : null, [qa, quote, drafting]);
  // updateQuoteAnalysis is what the deck panels call; here it must write to the
  // QUOTATION instead, so it is handed a ctx whose writer points at this row.
  const deckCtx = useMemo(() => Object.assign({}, ctx, {
    updateQuoteAnalysis: (pid, _id, fields) => ctx.updateQuoteRevisionDeck(pid, quote.id, fields),
  }), [ctx, quote.id]);

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2 flex-wrap">
        <button onClick={onBack} className="text-sm font-semibold text-[var(--leon-brown)]">← All quotations</button>
        <div className="flex-1" />
        {quote.isFinal ? <Badge tone="green">✓ On contract</Badge>
          : drafting ? <Badge tone="yellow">Drafting</Badge> : <Badge tone="brown">Submitted</Badge>}
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
        <div className="flex items-baseline gap-3 flex-wrap">
          <div className="text-lg font-bold">Quotation Rev {String(quote.revision).padStart(2, '0')}</div>
          <div className="text-xl font-bold text-[var(--leon-brown)]">{fmtMoney(quote.amount)}</div>
          <div className="text-xs text-[var(--leon-black)]/55">
            {quote.submittedDate ? 'Submitted ' + fmtDate(quote.submittedDate) : 'Created ' + fmtDate(quote.date)}
            {quote.analysisRevision ? ' · from Analysis Rev ' + quote.analysisRevision : ''}
          </div>
        </div>
        {quote.note ? <div className="text-xs text-[var(--leon-black)]/60 mt-1">{quote.note}</div> : null}

        {!qa ? (
          <div className="mt-3 text-sm text-[var(--leon-black)]/60">
            This quotation was entered by hand and has no analysis behind it, so there is no deck to
            build. Attach the PDF that was sent, below.
          </div>
        ) : leaks.length ? (
          <div className="mt-3 rounded-md bg-[var(--leon-red)]/10 border border-[var(--leon-red)]/30 p-2 text-xs">
            <b>Held back.</b> The client document still carries {leaks.join(', ')} — nothing can be viewed
            or sent until that is clear.
          </div>
        ) : (
          <div className="mt-3 flex flex-wrap gap-2">
            <QuoteDeckViewButton ctx={ctx} project={project} qa={deckQa} doc={doc} leaks={leaks} />
            <QuoteDeckPdfButton ctx={ctx} project={project} qa={deckQa} doc={doc} leaks={leaks} />
            {canEdit && (
              <Button size="sm" variant="ghost" onClick={() => {
                const d = clientQuoteToSlides(ctx, project, deckQa, doc);
                if (d) ctx.goSoftware('office:slides', { doc: d.id });
              }}>Open in LEON Presentation</Button>
            )}
            {drafting && editable ? (
              <Button size="sm" variant="primary" onClick={() => {
                if (confirm(`Submit Rev ${quote.revision} to the client?\n\nIt moves to Submitted, and the analysis behind it becomes read-only.`))
                  ctx.submitQuoteRevision(project.id, quote.id);
              }}>Submit to client →</Button>
            ) : null}
            {!drafting ? <QuoteEmailClientButton ctx={ctx} project={project} qa={deckQa} doc={doc} /> : null}
            {!drafting && !quote.isFinal && editable
              ? <Button size="sm" variant="outline" onClick={onConvert}>Convert to Contract</Button> : null}
            <ShareButton ctx={ctx} projectId={project.id} subjectKey={`quotation:${quote.id}`}
              subject={`${project.name} — Quotation Rev ${String(quote.revision).padStart(2, '0')}`}
              summary={fmtMoney(quote.amount)} />
          </div>
        )}
      </div>

      {qa && doc ? (
        <>
          <QuoteDeckSlidesPanel ctx={deckCtx} project={project} qa={deckQa} doc={doc} editable={canEdit} />
          <QuoteDeckPicturesPanel ctx={deckCtx} project={project} qa={deckQa} doc={doc} editable={canEdit} />
          <QuoteDeckDetailsPanel ctx={deckCtx} project={project} qa={deckQa} doc={doc} editable={canEdit} />
        </>
      ) : null}

      <Collapsible id={'qr-files-' + quote.id} title="Files on this quotation" defaultOpen={false}>
        <div className="grid sm:grid-cols-2 gap-3 text-xs">
          <Field label="What the client was sent">
            <FileField name={quote.clientFile} url={quote.clientFileUrl} editable={editable}
              onChange={(f, u) => ctx.updateQuoteRevision(project.id, quote.id, { clientFile: f, clientFileUrl: u })} />
          </Field>
          <Field label="Internal analysis">
            <FileField name={quote.internalAnalysisFile} url={quote.internalAnalysisFileUrl} editable={editable}
              onChange={(f, u) => ctx.updateQuoteRevision(project.id, quote.id, { internalAnalysisFile: f, internalAnalysisFileUrl: u })} />
          </Field>
        </div>
      </Collapsible>
      <ShareLog ctx={ctx} subjectKey={`quotation:${quote.id}`} />
    </div>
  );
}

const QUOTE_STAGE_TABS = [
  { key: 'overview',  label: 'Overview',  icon: '📊' },
  { key: 'drafting',  label: 'Drafting',  icon: '✏️' },
  { key: 'submitted', label: 'Submitted', icon: '📤' },
];
// The QUOTATIONS on a job — what the client was actually sent, as opposed to
// the analysis behind it. A quotation arrives here from an analysis revision
// ("Create quotation"), is arranged while DRAFTING, and moves to SUBMITTED the
// day it goes out. Converting to a contract happens from here too, because a
// contract is agreed against a quotation and never against our own working.
function SalesQuotesSubTab({ ctx, project }) {
  const [tab, setTab] = useState('overview');
  const [showRev, setShowRev] = useState(false);
  const [editRev, setEditRev] = useState(null);
  const [convertFor, setConvertFor] = useState(null);
  const [openId, setOpenId] = useState(null);
  const editable = ctx.canEdit('quotes');
  const all = project.quoteRevisions || [];
  const stageOf = q => q.stage || (q.isFinal || q.submittedDate ? 'submitted' : 'submitted');
  const drafting = all.filter(q => stageOf(q) === 'drafting');
  const submitted = all.filter(q => stageOf(q) !== 'drafting');
  const open = all.find(q => q.id === openId) || null;

  if (open) {
    return <QuoteRevisionDetail ctx={ctx} project={project} quote={open} editable={editable}
      onBack={() => setOpenId(null)} onConvert={() => setConvertFor(open)} />;
  }

  const list = (rows, empty) => rows.length === 0 ? <EmptyState text={empty} /> : (
    <table className="w-full text-xs">
      <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase">
        <th className="py-1">Rev</th><th className="py-1">Amount</th><th className="py-1">Date</th>
        <th className="py-1">From</th><th className="py-1">Note</th><th className="py-1">Status</th><th className="py-1"></th>
      </tr></thead>
      <tbody>{rows.map(q => (
        <tr key={q.id} className={`border-t border-[var(--leon-line)] ${q.isFinal ? 'bg-[var(--leon-cream)]' : ''}`}>
          <td className="py-1.5 font-semibold">R{q.revision}</td>
          <td className="py-1.5">{fmtMoney(q.amount)}</td>
          <td className="py-1.5">{fmtDate(q.submittedDate || q.date)}</td>
          <td className="py-1.5 text-[var(--leon-black)]/50">{q.analysisRevision ? `Analysis Rev ${q.analysisRevision}` : '—'}</td>
          <td className="py-1.5 text-[var(--leon-black)]/60">{q.note}</td>
          <td className="py-1.5">
            {q.isFinal ? <Badge tone="green">✓ On contract</Badge>
              : stageOf(q) === 'drafting' ? <Badge tone="yellow">Drafting</Badge>
              : <Badge tone="brown">Submitted</Badge>}
          </td>
          <td className="py-1.5 text-right whitespace-nowrap">
            <Button size="sm" variant="ghost" onClick={() => setOpenId(q.id)}>Open</Button>
            {!q.isFinal && editable && stageOf(q) !== 'drafting'
              ? <Button size="sm" variant="outline" onClick={() => setConvertFor(q)}>Convert to Contract</Button> : null}
            {q.isFinal && editable
              ? <Button size="sm" variant="ghost" onClick={() => ctx.undoConvertQuoteToContract(project.id, q.id)}>Undo</Button> : null}
          </td>
        </tr>
      ))}</tbody>
    </table>
  );

  return (
    <div className="space-y-3">
      <div className="flex items-center gap-2 flex-wrap">
        <Badge tone="neutral">{project.companyDepartment.join(' + ')}</Badge>
        <div className="flex-1" />
        {editable && <Button size="sm" variant="ghost" onClick={() => setShowRev(true)}>+ Add a quotation by hand</Button>}
      </div>
      <div className="flex gap-1 border-b border-[var(--leon-line)]">
        {QUOTE_STAGE_TABS.map(t => {
          const n = t.key === 'overview' ? all.length : t.key === 'drafting' ? drafting.length : submitted.length;
          return (
            <button key={t.key} onClick={() => setTab(t.key)}
              className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${tab === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
              <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>{t.label}
              <span className="ml-1.5 opacity-50">{n}</span>
            </button>
          );
        })}
      </div>

      {tab === 'overview' ? (
        <div className="space-y-2">
          <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
            {[['Quotations', all.length], ['Drafting', drafting.length], ['Submitted', submitted.length],
              ['Latest value', all.length ? fmtMoney(all[all.length - 1].amount) : '—']].map(([k, v]) => (
              <div key={k} className="rounded-lg border border-[var(--leon-line)] bg-white p-2">
                <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">{k}</div>
                <div className="text-lg font-bold">{v}</div>
              </div>
            ))}
          </div>
          {list(all, 'No quotations yet. Price the work under Quote Analysis, then press “Create quotation”.')}
        </div>
      ) : null}
      {tab === 'drafting' ? (
        <div className="space-y-2">
          <p className="text-xs text-[var(--leon-black)]/60">
            Arranged but not yet sent. Open one to lay out its deck, place the pictures and check it,
            then submit it — that is the moment the client holds it and the analysis behind it freezes.
          </p>
          {list(drafting, 'Nothing in drafting.')}
        </div>
      ) : null}
      {tab === 'submitted' ? (
        <div className="space-y-2">
          <p className="text-xs text-[var(--leon-black)]/60">
            Sent to the client. These are what a contract is agreed against, and what the follow-up
            queue chases.
          </p>
          {list(submitted, 'Nothing submitted yet.')}
        </div>
      ) : null}

      <AddQuoteRevisionModal open={showRev} onClose={() => setShowRev(false)} ctx={ctx} project={project} />
      <AddQuoteRevisionModal open={!!editRev} editRevision={editRev} onClose={() => setEditRev(null)} ctx={ctx} project={project} />
      <ConvertToContractModal open={!!convertFor} quote={convertFor} onClose={() => setConvertFor(null)} ctx={ctx} project={project} />
    </div>
  );
}

// Converting a quote to contract can suggest a payment schedule at the same
// time — each suggested term is saved with status "Suggested" and only
// becomes a real, billable term once Accounting approves it from Financials
// → Accounts Receivable (that tab is already Admin/Accounting-only, so no
// separate permission check is needed here beyond who can convert at all).
function ConvertToContractModal({ open, quote, onClose, ctx, project }) {
  const blank = { label: '', pct: '', trigger: '' };
  const [terms, setTerms] = useState([]);
  const [draft, setDraft] = useState(blank);
  useEffect(() => { if (open) { setTerms([]); setDraft(blank); } }, [open]);
  if (!quote) return null;
  function addDraftTerm() {
    if (!draft.label.trim() || !draft.pct) return;
    setTerms([...terms, { ...draft, pct: Number(draft.pct) }]);
    setDraft(blank);
  }
  function removeDraftTerm(i) { setTerms(terms.filter((_, idx) => idx !== i)); }
  function submit() {
    ctx.convertQuoteToContract(project.id, quote.id);
    terms.forEach(t => ctx.addPaymentTerm(project.id, { ...t, status: 'Suggested' }));
    onClose();
  }
  const pctTotal = terms.reduce((s, t) => s + t.pct, 0);
  return (
    <Modal open={open} onClose={onClose} wide title={`Convert Revision ${quote.revision} to Contract`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Convert to Contract</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Sets Revision {quote.revision} ({fmtMoney(quote.amount)}) as the awarded quotation and Original Contract Value.</p>
        <div>
          <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1.5">Suggest a Payment Schedule (optional)</p>
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">Each term below is saved as a suggestion — Accounting must approve it in Financials → Accounts Receivable before it becomes a real, billable term.</p>
          {terms.length > 0 && (
            <div className="space-y-1 mb-2">
              {terms.map((t, i) => (
                <div key={i} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-1.5 text-xs">
                  <span><strong>{t.label}</strong> · {t.pct}% · {t.trigger || '—'}</span>
                  <button onClick={() => removeDraftTerm(i)} className="text-[var(--leon-red)] font-semibold">✕</button>
                </div>
              ))}
              {pctTotal !== 100 && <p className="text-[11px] text-[var(--leon-red)] font-semibold">Suggested terms total {pctTotal}%, not 100%.</p>}
            </div>
          )}
          <div className="grid grid-cols-[2fr_1fr_2fr_auto] gap-2 items-end">
            <Field label="Label"><TextInput value={draft.label} onChange={e => setDraft({ ...draft, label: e.target.value })} placeholder="e.g. Deposit" /></Field>
            <Field label="%"><TextInput type="number" min="0" max="100" value={draft.pct} onChange={e => setDraft({ ...draft, pct: e.target.value })} /></Field>
            <Field label="Trigger"><TextInput value={draft.trigger} onChange={e => setDraft({ ...draft, trigger: e.target.value })} placeholder="e.g. Contract Signing" /></Field>
            <Button size="sm" variant="ghost" onClick={addDraftTerm}>+ Add</Button>
          </div>
        </div>
      </div>
    </Modal>
  );
}
// Split from Quotes: entries with a future next-follow-up date surface as
// "Scheduled" (with their assignee, if any — that's also who got the
// matching My To-Do reminder); the rest is just the full history.
function FollowUpsSubTab({ ctx, project }) {
  const [showFu, setShowFu] = useState(false);
  const editable = ctx.canEdit('quotes');
  const today = todayISO();
  // Which revision a chase was about, so "have we followed up on Rev 2?" is
  // answerable from the history rather than from memory.
  const revLabel = id => {
    const r = (project.quoteRevisions || []).find(x => x.id === id);
    return r ? `Rev ${r.revision}` : null;
  };
  const scheduled = project.quotationFollowUps.filter(f => f.nextFollowUp && f.nextFollowUp >= today).sort((a, b) => a.nextFollowUp.localeCompare(b.nextFollowUp));
  const history = [...project.quotationFollowUps].sort((a, b) => b.date.localeCompare(a.date));
  return (
    <div>
      <div className="flex items-center justify-between gap-3 mb-2 flex-wrap">
        <p className="text-sm text-[var(--leon-black)]/60">
          Chase a quotation and send the email from here — pick the revision you are following up on and it
          goes out branded, with your signature and a copy to you.
        </p>
        {editable && <Button size="sm" onClick={() => setShowFu(true)}>+ Log Follow-Up</Button>}
      </div>
      <Collapsible title="Scheduled Follow-Ups" count={scheduled.length}>
        {scheduled.length === 0 ? <EmptyState text="Nothing scheduled." /> : (
          <div className="space-y-1.5">
            {scheduled.map(f => (
              <div key={f.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2 text-xs">
                <div className="min-w-0">
                  <div className="flex items-center gap-2"><Badge tone="yellow">{fmtDate(f.nextFollowUp)}</Badge><span className="font-semibold">{f.method}</span></div>
                  <p className="text-[var(--leon-black)]/60 mt-1">{f.note}</p>
                </div>
                <span className="text-[var(--leon-black)]/50 shrink-0">{f.assigneeId ? personName(ctx.teamDirectory, f.assigneeId) : 'Unassigned'}</span>
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <Collapsible title="Follow-Up History" count={history.length}>
        {history.length === 0 ? <EmptyState text="No follow-ups logged." /> : (
          <div className="space-y-1.5">
            {history.map(f => (
              <div key={f.id} className="border border-[var(--leon-line)] rounded-lg px-3 py-2 text-xs">
                <div className="flex items-center gap-2 flex-wrap">
                  <Badge tone="neutral">{f.method}</Badge><span className="font-semibold">{fmtDate(f.date)}</span>
                  {revLabel(f.quoteRevisionId) && <Badge tone="brown">{revLabel(f.quoteRevisionId)}</Badge>}
                  {f.emailed && <span className="text-[var(--leon-brown)] font-semibold" title={`Emailed to ${f.sentTo}`}>📤 emailed</span>}
                  {f.assigneeId && <span className="text-[var(--leon-black)]/40">· {personName(ctx.teamDirectory, f.assigneeId)}</span>}
                  {f.nextFollowUp && <span className="text-[var(--leon-black)]/40">→ next {fmtDate(f.nextFollowUp)}</span>}
                </div>
                {f.sentTo && <div className="text-[var(--leon-black)]/40 mt-0.5">to {f.sentTo}</div>}
                <p className="text-[var(--leon-black)]/60 mt-1">{f.note}</p>
              </div>
            ))}
          </div>
        )}
      </Collapsible>
      <AddFollowUpModal open={showFu} onClose={() => setShowFu(false)} ctx={ctx} project={project} />
    </div>
  );
}
// Signed contract, the quote it was awarded from, contract value summary,
// and a read-only view of the payment schedule (edited from Financials →
// Accounts Receivable — not duplicated here as a second editable surface).
function ContractSubTab({ ctx, project }) {
  const editable = ctx.canEdit('quotes');
  const rcv = revisedContractValue(project);
  const awardedQuote = project.quoteRevisions.find(q => q.isFinal) || [...project.quoteRevisions].sort((a, b) => b.revision - a.revision)[0];
  const terms = paymentTermsWithAmounts(project);
  return (
    <div>
      <div className="flex items-center gap-2 mb-2"><Badge tone="neutral">{project.companyDepartment.join(' + ')}</Badge></div>
      <Collapsible title="Contract Value Summary">
        <div className="grid sm:grid-cols-2 gap-3">
          <div className="border border-[var(--leon-line)] rounded-lg p-3">
            <p className="text-xs text-[var(--leon-black)]/50 uppercase font-semibold">Original Contract Value</p>
            <p className="text-xl font-bold">{fmtMoney(project.originalContractValue)}</p>
          </div>
          <div className="border-2 border-[var(--leon-brown)] rounded-lg p-3 bg-[var(--leon-cream)]">
            <p className="text-xs text-[var(--leon-brown)] uppercase font-semibold">Revised Contract Value</p>
            <p className="text-xl font-bold">{fmtMoney(rcv)}</p>
          </div>
        </div>
      </Collapsible>
      <Collapsible title="Approved Quotation">
        {awardedQuote ? (
          <div className="grid sm:grid-cols-2 gap-3">
            {!awardedQuote.isFinal && <p className="sm:col-span-2 text-[11px] text-[var(--leon-black)]/40 italic">No revision has been explicitly converted to Contract yet — showing the latest revision. Use "Convert to Contract" on the Quotes tab to lock this in.</p>}
            <Field label="Revision"><p className="text-sm font-semibold">R{awardedQuote.revision} — {fmtMoney(awardedQuote.amount)}</p></Field>
            <Field label="Date"><p className="text-sm font-semibold">{fmtDate(awardedQuote.date)}</p></Field>
            <Field label="Client-Shared PDF"><FileField name={awardedQuote.clientFile} url={awardedQuote.clientFileUrl} onChange={() => {}} editable={false} /></Field>
            <Field label="Note"><p className="text-sm">{awardedQuote.note || '—'}</p></Field>
          </div>
        ) : <EmptyState text="No quote revisions on file yet." />}
      </Collapsible>
      <Collapsible title="Signed Contract">
        {/* The executed contract and the date it was signed. Both are facts
            about a document that already exists, so they are read constantly
            and changed almost never. */}
        <EditLock canEdit={editable} hint="Locked — press Edit to change the signed contract record.">
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Contract Document">
              <FileField name={project.contractFile} url={project.contractFileUrl} onChange={(fname, url) => ctx.updateProjectInfo(project.id, { contractFile: fname, contractFileUrl: url })} editable={editable} placeholder="No contract uploaded yet" />
              {/* The executed contract is the document the whole financial tab
                  rests on, so this is where "has it been signed, and by whom"
                  has to be answerable. Weighted as a CONTRACT, which is what
                  makes the screen say the in-app signature is not binding. */}
              {typeof SignSendButton === 'function' && project.contractFileUrl && (
                <div className="mt-1.5">
                  <SignSendButton ctx={ctx} label="Send the contract for signature"
                    source={{ kind: 'Contract', refId: `contract-${project.id}`, revision: null,
                      url: project.contractFileUrl, weight: 'contract',
                      projectId: project.id, projectName: project.name,
                      scopeId: null, scopeName: '' }} />
                </div>
              )}
            </Field>
            <Field label="Signed Date">
              {editable ? (
                <TextInput type="date" value={project.contractSignedDate || ''} onChange={e => ctx.updateProjectInfo(project.id, { contractSignedDate: e.target.value || null })} />
              ) : <p className="text-sm font-semibold">{project.contractSignedDate ? fmtDate(project.contractSignedDate) : '—'}</p>}
            </Field>
          </div>
        </EditLock>
      </Collapsible>
      <Collapsible title="Payment Schedule" count={terms.length}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">Read-only here — edited from Financials → Accounts Receivable.</p>
        {terms.length === 0 ? <EmptyState text="No payment terms defined yet." /> : (
          <div className="space-y-1.5">
            {terms.map(t => (
              <div key={t.id} className="flex items-center justify-between gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2 text-xs">
                <div className="min-w-0">
                  <p className="font-semibold">{t.label} <span className="text-[var(--leon-black)]/40 font-normal">· {t.pct}% · {t.trigger}</span></p>
                  <p className="text-[var(--leon-black)]/50">{fmtMoney(t.amount)}</p>
                </div>
                <StatusBadge status={t.status} />
              </div>
            ))}
          </div>
        )}
      </Collapsible>
    </div>
  );
}
function ChangeOrderTable({ ctx, project, rows, editable, onOpen, onEdit }) {
  return rows.length === 0 ? <EmptyState text="None yet." /> : (
    <table className="w-full text-xs">
      <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">#</th><th className="py-1">Amount</th><th className="py-1">Date</th><th className="py-1">Description</th><th className="py-1">File</th><th className="py-1">Status</th><th className="py-1"></th></tr></thead>
      <tbody>{rows.map(co => (
        <tr key={co.id} className="border-t border-[var(--leon-line)]">
          <td className="py-1.5 font-semibold cursor-pointer hover:underline" onClick={() => onOpen(co)}>{co.number || <span className="text-[var(--leon-black)]/40 italic font-normal">assigned on approval</span>}</td>
          <td className={`py-1.5 font-semibold ${co.amount < 0 ? 'text-[var(--leon-red)]' : ''}`}>{fmtMoney(co.amount)}</td>
          <td className="py-1.5">{fmtDate(co.date)}</td>
          <td className="py-1.5 text-[var(--leon-black)]/60">{co.description}</td>
          <td className="py-1.5"><FileField name={co.file} url={co.fileUrl} onChange={(fname, url) => ctx.updateChangeOrder(project.id, co.id, { file: fname, fileUrl: url })} editable={editable} /></td>
          <td className="py-1.5">
            {editable && co.status === 'Pending' ? (
              <div className="flex gap-1">
                <Button size="sm" onClick={() => ctx.setChangeOrderStatus(project.id, co.id, 'Approved')}>Approve</Button>
                <Button size="sm" variant="ghost" onClick={() => ctx.setChangeOrderStatus(project.id, co.id, 'Rejected')}>Reject</Button>
              </div>
            ) : editable && ['Approved', 'Rejected'].includes(co.status) ? (
              <div className="flex items-center gap-2">
                <StatusBadge status={co.status} />
                <button onClick={() => ctx.undoChangeOrderApproval(project.id, co.id)} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline">Undo</button>
              </div>
            ) : <StatusBadge status={co.status} />}
          </td>
          <td className="py-1.5">{editable && <button onClick={() => onEdit(co)} className="text-[var(--leon-brown)] font-semibold">✎ Edit</button>}</td>
        </tr>
      ))}</tbody>
    </table>
  );
}
function ChangeOrderDetailModal({ open, co, onClose, project }) {
  if (!co) return null;
  const scope = co.scopeId ? project.scopes.find(s => s.id === co.scopeId) : null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`${co.type} ${co.number || '(pending approval)'}`} printable
      fields={[
        { label: 'Type', value: co.type }, { label: 'Status', value: co.status },
        { label: 'Amount', value: fmtMoney(co.amount) }, { label: 'Date', value: fmtDate(co.date) },
        { label: 'Scope', value: scope ? scope.name : '—' }, { label: 'Description', value: co.description },
      ]}
      attachments={co.fileUrl ? [{ name: co.file, url: co.fileUrl }] : []}
    />
  );
}
function ChangeOrdersSubTab({ ctx, project }) {
  const [showCo, setShowCo] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const [editFor, setEditFor] = useState(null);
  const editable = ctx.canEdit('quotes');
  const rows = project.changeOrders.filter(c => c.type === 'Change Order');
  return (
    <div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">Each Change Order is assigned its automated CO number automatically once approved — not at submission.</p>
      <Collapsible title="Change Orders" count={rows.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowCo(true)}>+ Add Change Order</Button>}>
        <ChangeOrderTable ctx={ctx} project={project} rows={rows} editable={editable} onOpen={setDetailFor} onEdit={setEditFor} />
      </Collapsible>
      <AddChangeOrderModal open={showCo} onClose={() => setShowCo(false)} ctx={ctx} project={project} fixedType="Change Order" />
      <AddChangeOrderModal open={!!editFor} editOrder={editFor} onClose={() => setEditFor(null)} ctx={ctx} project={project} fixedType="Change Order" />
      <ChangeOrderDetailModal open={!!detailFor} co={detailFor} onClose={() => setDetailFor(null)} project={project} />
    </div>
  );
}
function BackChargesSubTab({ ctx, project }) {
  const [showBc, setShowBc] = useState(false);
  const [detailFor, setDetailFor] = useState(null);
  const [editFor, setEditFor] = useState(null);
  const editable = ctx.canEdit('quotes');
  const rows = project.changeOrders.filter(c => c.type === 'Back Charge');
  return (
    <div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">Each Back Charge is assigned its automated BC number automatically once approved — not at submission.</p>
      <Collapsible title="Back-charges" count={rows.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowBc(true)}>+ Add Back Charge</Button>}>
        <ChangeOrderTable ctx={ctx} project={project} rows={rows} editable={editable} onOpen={setDetailFor} onEdit={setEditFor} />
      </Collapsible>
      <AddChangeOrderModal open={showBc} onClose={() => setShowBc(false)} ctx={ctx} project={project} fixedType="Back Charge" />
      <AddChangeOrderModal open={!!editFor} editOrder={editFor} onClose={() => setEditFor(null)} ctx={ctx} project={project} fixedType="Back Charge" />
      <ChangeOrderDetailModal open={!!detailFor} co={detailFor} onClose={() => setDetailFor(null)} project={project} />
    </div>
  );
}
function AddQuoteRevisionModal({ open, onClose, ctx, project, editRevision }) {
  const isEdit = !!editRevision;
  const blank = { amount: '', date: todayISO(), clientFile: '', clientFileUrl: null, internalAnalysisFile: '', internalAnalysisFileUrl: null, note: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (editRevision) {
      setForm({
        amount: editRevision.amount, date: editRevision.date, clientFile: editRevision.clientFile, clientFileUrl: editRevision.clientFileUrl,
        internalAnalysisFile: editRevision.internalAnalysisFile, internalAnalysisFileUrl: editRevision.internalAnalysisFileUrl, note: editRevision.note,
      });
    } else {
      setForm(blank);
    }
  }, [open, editRevision]);
  function submit() {
    if (!form.amount) return;
    if (isEdit) ctx.updateQuoteRevision(project.id, editRevision.id, { ...form, amount: Number(form.amount) });
    else ctx.addQuoteRevision(project.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={isEdit ? `Edit Quote Revision ${editRevision.revision}` : 'Add Quote Revision'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Revision'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount"><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        </div>
        <Field label="Client-Shared PDF" hint="The actual quote PDF sent to the client."><FileField name={form.clientFile} url={form.clientFileUrl} onChange={(fname, url) => setForm({ ...form, clientFile: fname, clientFileUrl: url })} editable /></Field>
        <Field label="Internal Quote Analysis File" hint="Internal-only — margin/cost analysis behind this revision."><FileField name={form.internalAnalysisFile} url={form.internalAnalysisFileUrl} onChange={(fname, url) => setForm({ ...form, internalAnalysisFile: fname, internalAnalysisFileUrl: url })} editable /></Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}
// Chasing a quote is the most repeated job in sales, and it was a note in a
// box. Now the same modal can also SEND: pick which quotation you are chasing,
// pick who it goes to, and the email queues through the ordinary share
// machinery — same branding, same signature, same sender Cc, same honest
// outbox. It is never a separate mail path with its own rules.
function AddFollowUpModal({ open, onClose, ctx, project }) {
  const revs = [...(project.quoteRevisions || [])].sort((a, b) => b.revision - a.revision);
  const latest = revs[0];
  const blank = {
    date: todayISO(), method: 'Email', note: '', nextFollowUp: '', assigneeId: '',
    quoteRevisionId: latest ? latest.id : '', sendEmail: true, to: '', message: '',
  };
  const [form, setForm] = useState(blank);
  const [sent, setSent] = useState(null);
  useEffect(() => { if (open) { setForm(blank); setSent(null); } }, [open]);

  // Who a quote is chased with. The Estimator leads, because that is who a bid
  // goes to; everyone else on the job follows.
  const contacts = useMemo(() => {
    const out = [];
    const seen = new Set();
    const push = (label, c) => {
      if (!c || !c.email || seen.has(c.email.toLowerCase())) return;
      seen.add(c.email.toLowerCase());
      out.push({ key: c.email, label, name: c.person || c.company || label, email: c.email, company: c.company || '' });
    };
    ['Estimator', 'General Contractor', 'Owner', 'Developer', 'Architect', 'Designer', 'Billing Contact']
      .forEach(r => push(r, (project.contacts || {})[r]));
    (project.additionalContacts || []).forEach(c => push(c.label || 'Additional Contact', c));
    return out;
  }, [project.contacts, project.additionalContacts]);

  useEffect(() => {
    if (open && contacts.length && !form.to) setForm(f => ({ ...f, to: contacts[0].key }));
  }, [open, contacts.length]);

  const rev = revs.find(r => r.id === form.quoteRevisionId);
  const recipient = contacts.find(c => c.key === form.to);
  const emailing = form.method === 'Email' && form.sendEmail;
  const subject = rev
    ? `Following up — ${project.name} quotation Rev ${rev.revision}`
    : `Following up — ${project.name}`;
  const summary = rev
    ? `Quotation Rev ${rev.revision}${rev.amount ? `, ${fmtMoney(rev.amount)}` : ''}${rev.date ? `, issued ${fmtDate(rev.date)}` : ''}.`
    : 'Quotation follow-up.';

  // The plain-text draft the mail client actually receives.
  const mail = useMemo(() => buildQuoteFollowUpEmail({
    subject,
    recipientName: recipient && recipient.name,
    senderName: ctx.currentUserName, senderTitle: personTitle(ctx.currentUser),
    senderSignature: ctx.currentUser && ctx.currentUser.emailSignature,
    senderSignatureImage: ctx.currentUser && ctx.currentUser.emailSignatureImage,
    company: ctx.companyProfile, projectName: project.name,
    revLabel: rev ? `Rev ${rev.revision}` : '',
    revAmount: rev && rev.amount ? fmtMoney(rev.amount) : '',
    revDate: rev && rev.date ? fmtDate(rev.date) : '',
    message: form.message || form.note,
  }), [recipient, rev, form.message, form.note, project.name, subject]);
  const mailText = mail.body;

  function openInMailClient() {
    if (!recipient) return;
    window.location.href = buildMailtoUrl({
      to: recipient.email, cc: (ctx.currentUser && ctx.currentUser.email) || '',
      subject, body: mailText,
    });
  }

  function submit() {
    let queued = 0;
    if (emailing && recipient) {
      // The queued copy is the RECORD — branded, logged, and there whether or
      // not the mail client ever opened.
      ctx.fileOutgoingEmail({
        to: recipient.email, toName: recipient.name, subject,
        body: mailText, html: mail.html, projectId: project.id,
      });
      queued = 1;
      // ...and this is the part that actually sends.
      openInMailClient();
    }
    ctx.addFollowUp(project.id, {
      date: form.date, method: form.method, note: form.note,
      nextFollowUp: form.nextFollowUp || null,
      assigneeId: form.assigneeId || null,
      quoteRevisionId: form.quoteRevisionId || null,
      sentTo: emailing && recipient ? `${recipient.name} <${recipient.email}>` : null,
      emailed: !!queued,
    });
    if (queued) { setSent({ to: recipient.name, email: recipient.email }); return; }
    onClose();
  }

  if (sent) {
    return (
      <Modal open={open} onClose={onClose} title="Follow-up sent"
        footer={<>
          <Button variant="ghost" onClick={() => { navigator.clipboard.writeText(mailText); }}>Copy the text</Button>
          <Button variant="ghost" onClick={openInMailClient}>Open it again</Button>
          <Button onClick={onClose}>Done</Button>
        </>}>
        <div className="space-y-3 text-sm">
          <div className="text-3xl">📧</div>
          <p><b>Your email app should have opened with the message to {sent.to} ready to send.</b></p>
          <p className="text-[var(--leon-black)]/60">
            It goes from your own address with a copy to you, so it lands in your Sent folder like any
            other email. <b>Press send in your mail app</b> — the Hub cannot send it for you.
          </p>
          <p className="text-[var(--leon-black)]/60">
            If nothing opened, this browser has no mail app set up. Use <b>Copy the text</b> and paste it
            into {sent.email} yourself.
          </p>
          <div className="rounded border border-[var(--leon-line)] bg-[var(--leon-cream)] p-2.5 text-xs text-[var(--leon-black)]/60">
            The follow-up is logged against the quotation either way, and a branded copy is in the
            outbox as the record.
          </div>
        </div>
      </Modal>
    );
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Quotation Follow-Up"
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={submit} disabled={emailing && !recipient}>
          {emailing ? '📧 Log and open the email' : 'Log follow-up'}
        </Button>
      </>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Method">
            <Select value={form.method} onChange={e => setForm({ ...form, method: e.target.value })}>
              {['Email', 'Call', 'Meeting', 'Text'].map(m => <option key={m}>{m}</option>)}
            </Select>
          </Field>
          <Field label="Which quotation" hint={revs.length ? 'What you are chasing.' : 'No quote revisions on this job yet.'}>
            <Select value={form.quoteRevisionId} disabled={!revs.length}
              onChange={e => setForm({ ...form, quoteRevisionId: e.target.value })}>
              <option value="">— none —</option>
              {revs.map(r => (
                <option key={r.id} value={r.id}>
                  Rev {r.revision}{r.amount ? ` — ${fmtMoney(r.amount)}` : ''}{r.date ? ` (${fmtDate(r.date)})` : ''}{r.isFinal ? ' ★' : ''}
                </option>
              ))}
            </Select>
          </Field>
        </div>

        <Field label="Note" hint="What was said, or what you are asking for.">
          <TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
        </Field>

        {form.method === 'Email' && (
          <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3 space-y-3">
            <label className="flex items-center gap-2 text-sm font-semibold">
              <input type="checkbox" checked={form.sendEmail} onChange={e => setForm({ ...form, sendEmail: e.target.checked })} />
              Send this follow-up by email
            </label>
            {form.sendEmail && (
              <>
                {!contacts.length ? (
                  <p className="text-sm text-red-700">
                    This job has no contact with an email address. Add one under <b>Project Information &rarr; Contacts</b> —
                    the <b>Estimator</b> is usually who a quotation is chased with.
                  </p>
                ) : (
                  <>
                    <Field label="To">
                      <Select value={form.to} onChange={e => setForm({ ...form, to: e.target.value })}>
                        {contacts.map(c => (
                          <option key={c.key} value={c.key}>
                            {c.label}: {c.name} — {c.email}
                          </option>
                        ))}
                      </Select>
                    </Field>
                    <Field label="Message" hint="Added above the quotation details in the email.">
                      <TextArea rows={3} value={form.message} placeholder={`Just following up on our quotation${rev ? ` (Rev ${rev.revision})` : ''} — happy to walk through it or adjust anything.`}
                        onChange={e => setForm({ ...form, message: e.target.value })} />
                    </Field>
                    <div className="rounded border border-[var(--leon-line)] bg-white p-2.5 text-xs">
                      <div className="font-semibold text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">What will queue</div>
                      <div><b>Subject:</b> {subject}</div>
                      <div><b>To:</b> {recipient ? `${recipient.name} <${recipient.email}>` : '—'}</div>
                      <div><b>Cc:</b> you</div>
                      <div className="mt-1 text-[var(--leon-black)]/60">{summary}</div>
                      <div className="mt-1.5 text-[var(--leon-black)]/45">
                        Opens in your own mail app, already written, so it sends from your address and
                        lands in your Sent folder. You press send. A branded copy is filed in the outbox
                        as the record — the Hub itself has no mail server and cannot send for you.
                      </div>
                      <div className="mt-2"><BrandedEmailPreview html={mail.html} text={mailText} /></div>
                      {mailText.length > MAILTO_SAFE_CHARS && (
                        <div className="mt-1 text-[11px] text-amber-700">
                          This is long for a pre-filled draft — some mail apps truncate. Shorten the message, or
                          use <b>Copy the text</b> after logging.
                        </div>
                      )}
                    </div>
                  </>
                )}
              </>
            )}
          </div>
        )}

        <div className="grid grid-cols-2 gap-3">
          <Field label="Next follow-up date (optional)">
            <TextInput type="date" value={form.nextFollowUp} onChange={e => setForm({ ...form, nextFollowUp: e.target.value })} />
          </Field>
          <Field label="Assigned to" hint={form.nextFollowUp ? 'Adds a reminder to their My To-Do' : undefined}>
            <Select value={form.assigneeId} onChange={e => setForm({ ...form, assigneeId: e.target.value })}>
              <option value="">— unassigned —</option>
              {ctx.teamDirectory.filter(p => p.active).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
        </div>
      </div>
    </Modal>
  );
}
function AddChangeOrderModal({ open, onClose, ctx, project, fixedType, editOrder }) {
  const isEdit = !!editOrder;
  const blank = { type: fixedType || 'Change Order', amount: '', date: todayISO(), file: '', fileUrl: null, description: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (editOrder) setForm({ type: editOrder.type, amount: Math.abs(editOrder.amount), date: editOrder.date, file: editOrder.file, fileUrl: editOrder.fileUrl, description: editOrder.description });
    else setForm(blank);
  }, [open, editOrder]);
  function submit() {
    if (!form.amount) return;
    const amount = form.type === 'Back Charge' ? -Math.abs(Number(form.amount)) : Math.abs(Number(form.amount));
    if (isEdit) ctx.updateChangeOrder(project.id, editOrder.id, { ...form, amount });
    else ctx.addChangeOrder(project.id, { ...form, amount: Number(form.amount) });
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={isEdit ? `Edit ${editOrder.type} ${editOrder.number || ''}` : (fixedType ? `Add ${fixedType}` : 'Add Change Order / Back Charge')} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{isEdit ? 'Save Changes' : 'Add Entry'}</Button></>}>
      <div className="space-y-3">
        {!fixedType && <Field label="Type"><Select value={form.type} onChange={e => setForm({ ...form, type: e.target.value })}><option>Change Order</option><option>Back Charge</option></Select></Field>}
        <Field label="Amount" hint={form.type === 'Back Charge' ? 'Enter as a positive number — stored as negative automatically.' : undefined}>
          <TextInput type="number" min="0" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} />
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        </div>
        <Field label="Description / Reason"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- Financials (financial gated) -------------------------------------
// ---- Financials hub: Accounts Payable / Accounts Receivable / AIA Billing ----
// Ordered money-in before money-out: the billing that raises an invoice,
// then what's owed to us, then what we owe — payables, freight, and the
// one-off costs that don't come through procurement. Issues sits last as the
// exception list rather than a step in that flow.
const FINANCIALS_SUBTABS = [
  { key: 'billing', label: 'AIA Billing', icon: '🏦' },
  { key: 'ar', label: 'Accounts Receivable', icon: '📥' },
  { key: 'ap', label: 'Accounts Payable', icon: '📤' },
  { key: 'freight', label: 'Freight Invoices', icon: '🚚' },
  { key: 'misc', label: 'Misc Invoices', icon: '🧾' },
  { key: 'issues', label: 'Issues', icon: '⚠️' },
];
function FinancialsHubTab({ ctx, project, pendingNav }) {
  const [sub, setSub] = useState((pendingNav && pendingNav.subtab) || 'ar');
  useEffect(() => { if (pendingNav && pendingNav.subtab) setSub(pendingNav.subtab); }, [pendingNav]);
  const openIssues = project.financialIssues.filter(i => i.status === 'Open');
  return (
    <div>
      <div className="flex gap-1 mb-4 border-b border-[var(--leon-line)] flex-wrap">
        {FINANCIALS_SUBTABS.map(t => (
          <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn shrink-0 whitespace-nowrap px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>
            {t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}{t.key === 'issues' && openIssues.length > 0 ? ` (${openIssues.length})` : ''}
          </button>
        ))}
      </div>
      <HubTools />
      {sub === 'ap' && <ProjectAccountsPayableSubTab ctx={ctx} project={project} />}
      {/* The same record Sales sets, mirrored where the money is actually
          billed — one fact, two places that need it, never two copies. */}
      {sub === 'ar' && <>
        <div className="mb-4"><ProjectTaxPanel ctx={ctx} project={project} mirroredFrom="Sales Hub" /></div>
        <AccountsReceivableSubTab ctx={ctx} project={project} />
      </>}
      {sub === 'freight' && <ProjectFreightInvoicesSubTab ctx={ctx} project={project} />}
      {sub === 'misc' && <ProjectMiscInvoicesSubTab ctx={ctx} project={project} />}
      {sub === 'billing' && <BillingTab ctx={ctx} project={project} />}
      {sub === 'issues' && <FinancialIssuesSubTab ctx={ctx} project={project} />}
    </div>
  );
}
// Freight and Misc invoices are both AP invoices under the hood (partyType
// 'Freight'/'Miscellaneous') — filtered straight out of this project's own
// apInvoices, same OpenInvoicesTable/ApInvoiceDetailModal every other AP
// list already uses, so approval/payment work identically everywhere.
function ProjectFreightInvoicesSubTab({ ctx, project }) {
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);
  const invoices = liveApInvoices(project).filter(i => i.partyType === 'Freight').map(inv => ({ ...inv, projectName: project.name }));
  const dash = apDashboardSummary(invoices);
  return (
    <div>
      <div className="grid sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-5">
        <StatBox label="Open Bills" value={String(dash.totalOpenBills)} />
        <StatBox label="Open Balance" value={fmtMoney(dash.totalOpenBalance)} />
        <StatBox label="Past Due" value={fmtMoney(dash.totalPastDue)} tone={dash.totalPastDue > 0 ? 'red' : 'green'} />
        <StatBox label="Pending Approval" value={String(dash.pendingApproval.length)} tone={dash.pendingApproval.length ? 'yellow' : undefined} />
        <StatBox label="Disputed / On Hold" value={String(dash.disputedOnHold.length)} tone={dash.disputedOnHold.length ? 'red' : undefined} />
      </div>
      <Collapsible title="Freight Invoices" count={invoices.length} right={ctx.canEdit('accountsPayable') && <Button size="sm" onClick={() => setAddInvoice(true)}>+ Add Freight Invoice</Button>}>
        <OpenInvoicesTable ctx={ctx} invoices={invoices} onOpen={setOpenInvoice} />
      </Collapsible>
      <AddFreightInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} />
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}
function ProjectMiscInvoicesSubTab({ ctx, project }) {
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);
  const invoices = liveApInvoices(project).filter(i => i.partyType === 'Miscellaneous').map(inv => ({ ...inv, projectName: project.name }));
  const dash = apDashboardSummary(invoices);
  return (
    <div>
      <div className="grid sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-5">
        <StatBox label="Open Bills" value={String(dash.totalOpenBills)} />
        <StatBox label="Open Balance" value={fmtMoney(dash.totalOpenBalance)} />
        <StatBox label="Past Due" value={fmtMoney(dash.totalPastDue)} tone={dash.totalPastDue > 0 ? 'red' : 'green'} />
        <StatBox label="Pending Approval" value={String(dash.pendingApproval.length)} tone={dash.pendingApproval.length ? 'yellow' : undefined} />
        <StatBox label="Disputed / On Hold" value={String(dash.disputedOnHold.length)} tone={dash.disputedOnHold.length ? 'red' : undefined} />
      </div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3">For job-related expenses that don't go through procurement (no vendor estimate/PO/PI) — permits, rentals, cleaning, and similar. Every one of these also counts toward the Profitability tab's Unplanned Business Costs report.</p>
      <Collapsible title="Miscellaneous Invoices" count={invoices.length} right={ctx.canEdit('accountsPayable') && <Button size="sm" onClick={() => setAddInvoice(true)}>+ Add Invoice</Button>}>
        <OpenInvoicesTable ctx={ctx} invoices={invoices} onOpen={setOpenInvoice} />
      </Collapsible>
      <AddMiscInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} project={project} />
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}
// New in Phase 7 — a payment issue raised against the project itself
// (distinct from disputing a single AP invoice), optionally holding all
// Delivery or Installation scheduling until resolved (see
// projectPaymentHold, checked from those two Hubs).
function FinancialIssuesSubTab({ ctx, project }) {
  const canReport = ['Admin', 'Accounting'].includes(ctx.currentRole);
  const [showAdd, setShowAdd] = useState(false);
  const open = project.financialIssues.filter(i => i.status === 'Open');
  const resolved = project.financialIssues.filter(i => i.status !== 'Open');
  return (
    <div>
      <div className="flex justify-end mb-2">{canReport && <Button size="sm" onClick={() => setShowAdd(true)}>+ Report Payment Issue</Button>}</div>
      <Collapsible title="Open" count={open.length}>
        {open.length === 0 ? <EmptyState text="No open financial issues." /> : (
          <div className="space-y-2">{open.map(i => <FinancialIssueRow key={i.id} i={i} ctx={ctx} project={project} canReport={canReport} />)}</div>
        )}
      </Collapsible>
      <Collapsible title="Resolved" count={resolved.length}>
        {resolved.length === 0 ? <EmptyState text="None yet." /> : (
          <div className="space-y-2">{resolved.map(i => <FinancialIssueRow key={i.id} i={i} ctx={ctx} project={project} canReport={canReport} />)}</div>
        )}
      </Collapsible>
      <AddFinancialIssueModal open={showAdd} onClose={() => setShowAdd(false)} ctx={ctx} project={project} />
    </div>
  );
}
function FinancialIssueRow({ i, ctx, project, canReport }) {
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <div className="flex items-center justify-between gap-2 flex-wrap mb-1">
        <div className="flex items-center gap-2 flex-wrap">
          <StatusBadge status={i.status} />
          {i.holdScope && <Badge tone="red">⚠ Hold — Payment Not Received ({i.holdScope})</Badge>}
        </div>
        {canReport && i.status === 'Open' && <Button size="sm" variant="ghost" onClick={() => ctx.setFinancialIssueStatus(project.id, i.id, 'Resolved')}>Mark Resolved</Button>}
      </div>
      <p className="text-sm">{i.description}</p>
      {i.planOfAction && <p className="text-xs text-[var(--leon-black)]/60 mt-1">Plan of Action: {i.planOfAction}</p>}
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-1">
        Raised by {i.createdBy}, {fmtDate(i.createdDate)}{i.assigneeIds.length > 0 ? ` · Assigned: ${i.assigneeIds.map(id => personName(ctx.teamDirectory, id)).join(', ')}` : ''}
      </p>
    </div>
  );
}
function AddFinancialIssueModal({ open, onClose, ctx, project }) {
  const blank = { description: '', planOfAction: '', assigneeIds: [], holdScope: 'None' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(blank); }, [open]);
  function toggleAssignee(id) { setForm(f => ({ ...f, assigneeIds: f.assigneeIds.includes(id) ? f.assigneeIds.filter(x => x !== id) : [...f.assigneeIds, id] })); }
  function submit() { if (!form.description.trim()) return; ctx.addFinancialIssue(project.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Report Payment Issue" wide footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Report Issue</Button></>}>
      <div className="space-y-3">
        <Field label="Description"><TextArea rows={2} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} /></Field>
        <Field label="Plan of Action"><TextArea rows={2} value={form.planOfAction} onChange={e => setForm({ ...form, planOfAction: e.target.value })} /></Field>
        <Field label="Assign To" hint="Multiple people can be assigned.">
          <div className="flex flex-wrap gap-1.5 border border-[var(--leon-line)] rounded-lg p-2">
            {ctx.teamDirectory.filter(p => p.active).map(p => (
              <label key={p.id} className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border cursor-pointer ${form.assigneeIds.includes(p.id) ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
                <input type="checkbox" className="hidden" checked={form.assigneeIds.includes(p.id)} onChange={() => toggleAssignee(p.id)} /> {p.name}
              </label>
            ))}
          </div>
        </Field>
        <Field label="Hold Payment Not Received" hint="Blocks scheduling in the selected Hub until this issue is resolved.">
          <Select value={form.holdScope} onChange={e => setForm({ ...form, holdScope: e.target.value })}>
            {FINANCIAL_ISSUE_HOLD_SCOPES.map(s => <option key={s}>{s}</option>)}
          </Select>
        </Field>
      </div>
    </Modal>
  );
}
// Project-scoped Accounts Payable — same underlying invoice data and shared
// components as the company-wide AP view, filtered to this one project so
// there's no separate system to keep in sync.
function ProjectAccountsPayableSubTab({ ctx, project }) {
  const [addInvoice, setAddInvoice] = useState(false);
  const [openInvoice, setOpenInvoice] = useState(null);
  const invoices = liveApInvoices(project).map(inv => ({ ...inv, projectName: project.name }));
  const dash = apDashboardSummary(invoices);
  return (
    <div>
      <div className="grid sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-5">
        <StatBox label="Open Bills" value={String(dash.totalOpenBills)} />
        <StatBox label="Open Balance" value={fmtMoney(dash.totalOpenBalance)} />
        <StatBox label="Past Due" value={fmtMoney(dash.totalPastDue)} tone={dash.totalPastDue > 0 ? 'red' : 'green'} />
        <StatBox label="Pending Approval" value={String(dash.pendingApproval.length)} tone={dash.pendingApproval.length ? 'yellow' : undefined} />
        <StatBox label="Disputed / On Hold" value={String(dash.disputedOnHold.length)} tone={dash.disputedOnHold.length ? 'red' : undefined} />
      </div>
      <Collapsible title="Invoices (Vendor / Freight / Subcontractor)" count={invoices.length} right={ctx.canEdit('accountsPayable') && <Button size="sm" onClick={() => setAddInvoice(true)}>+ Add Invoice</Button>}>
        <OpenInvoicesTable ctx={ctx} invoices={invoices} onOpen={setOpenInvoice} />
      </Collapsible>
      <AddApInvoiceModal open={addInvoice} onClose={() => setAddInvoice(false)} ctx={ctx} defaultProjectId={project.id} />
      <ApInvoiceDetailModal invoiceId={openInvoice} onClose={() => setOpenInvoice(null)} ctx={ctx} />
    </div>
  );
}
function AccountsReceivableSubTab({ ctx, project }) {
  const editable = ctx.canEdit('financials');
  const [showReq, setShowReq] = useState(false);
  const [showTerm, setShowTerm] = useState(false);
  const [reqDetailFor, setReqDetailFor] = useState(null);
  // Receipt + bank-hold capture, shared by payment terms and AIA requisitions.
  const [receiptFor, setReceiptFor] = useState(null);
  const terms = paymentTermsWithAmounts(project);
  const prof = profitability(project);
  const totalRetainage = totalRetainageHeld(project);
  const pctSum = project.paymentTerms.reduce((s, t) => s + t.pct, 0);

  return (
    <div>
      <div className="flex items-center gap-2 mb-2"><Badge tone="neutral">{project.companyDepartment.join(' + ')}</Badge></div>

      <Collapsible title="Payment Terms (Receivables)" count={terms.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowTerm(true)}>+ Add Term</Button>}>
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">Calculated against the revised contract value of {fmtMoney(prof.revisedContractValue)}. {pctSum !== 100 && <span className="text-[var(--leon-red)] font-semibold">Terms currently total {pctSum}%, not 100%.</span>}</p>
        <div className="space-y-1.5">
          {terms.map(t => (
            <div key={t.id} className={`flex items-center justify-between gap-2 border rounded-lg px-3 py-2 ${t.status === 'Suggested' ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)]'}`}>
              <div className="min-w-0 flex-1">
                <div className="flex items-center gap-2">
                  <p className="text-sm font-semibold">{t.label}</p>
                  {editable ? (
                    <TextInput type="number" min="0" max="100" defaultValue={t.pct} onBlur={e => ctx.updatePaymentTerm(project.id, t.id, { pct: Number(e.target.value) })} className="!w-16 !py-0.5 !text-xs" />
                  ) : <span className="text-[var(--leon-black)]/40 text-xs">{t.pct}%</span>}
                  <span className="text-[var(--leon-black)]/40 text-xs">· {t.trigger}</span>
                </div>
                <p className="text-xs text-[var(--leon-black)]/50">{fmtMoney(t.amount)}</p>
                {/* The budgeting date. Accounting and Admin only — it is a
                    planning figure, not a promise to the client. Left blank it
                    follows the schedule; once the money lands it locks to the
                    real received date and stops being editable. */}
                {ctx.canSeeAccountingHub && (() => {
                  const paid = t.status === 'Paid' || !!t.receivedDate;
                  const { date } = paymentTermDate(project, t);
                  if (paid) {
                    return (
                      <>
                        <p className="text-xs mt-1 text-[var(--leon-green)] font-semibold">
                          &#10003; Paid {fmtDate(t.receivedDate || date)}{t.receivedAmount != null ? ` \u00b7 ${fmtMoney(t.receivedAmount)}` : ''}
                          {t.receiptReference ? ` \u00b7 ref ${t.receiptReference}` : ''}
                          {editable && <button onClick={() => setReceiptFor({ kind: 'paymentTerm', record: t, label: t.label, amount: t.amount })}
                            className="ml-2 font-semibold text-[var(--leon-brown)] hover:underline">edit</button>}
                        </p>
                        <BankHoldPanel ctx={ctx} projectId={project.id} kind="paymentTerm" record={t} />
                      </>
                    );
                  }
                  const flag = cashEventFlag({ actual: false, date, direction: 'in' }, todayISO());
                  return (
                    <div className="flex items-center gap-1.5 mt-1 flex-wrap">
                      <span className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/40">Expected payment</span>
                      <TextInput type="date" value={t.expectedDate || ''}
                        onChange={e => ctx.postponePaymentTerm(project.id, t.id, e.target.value || null)}
                        className="!w-36 !py-0.5 !text-xs" />
                      {!t.expectedDate && date && <span className="text-[10px] text-[var(--leon-black)]/40 italic">following the schedule &mdash; {fmtDate(date)}</span>}
                      {!t.expectedDate && !date && <span className="text-[10px] text-[var(--leon-black)]/40 italic">no date yet</span>}
                      {date && flag.icon && (
                        <span className={`text-[10px] font-semibold ${flag.tone === 'red' ? 'text-[var(--leon-red)]' : 'text-[var(--leon-yellow)]'}`}>
                          {flag.icon} {flag.label}
                        </span>
                      )}
                      {t.expectedDateOriginal && t.expectedDateOriginal !== t.expectedDate && (
                        <span className="text-[10px] text-[var(--leon-black)]/40">moved from {fmtDate(t.expectedDateOriginal)}</span>
                      )}
                      {editable && (
                        <button onClick={() => setReceiptFor({ kind: 'paymentTerm', record: t, label: t.label, amount: t.amount })}
                          className="text-[10px] font-bold uppercase tracking-wide text-[var(--leon-brown)] hover:underline">
                          Record payment
                        </button>
                      )}
                      {(t.expectedDateHistory || []).length > 0 && (
                        <details className="w-full">
                          <summary className="text-[10px] text-[var(--leon-brown)] font-semibold cursor-pointer">
                            {t.expectedDateHistory.length} date change{t.expectedDateHistory.length === 1 ? '' : 's'}
                          </summary>
                          <ForecastDateHistory history={t.expectedDateHistory} label="Moved" />
                        </details>
                      )}
                    </div>
                  );
                })()}
              </div>
              {t.status === 'Suggested' && editable ? (
                <div className="flex items-center gap-2">
                  <Badge tone="yellow">Suggested — Awaiting Accounting Approval</Badge>
                  <Button size="sm" onClick={() => ctx.setPaymentTermStatus(project.id, t.id, 'Not Due')}>Approve</Button>
                  <Button size="sm" variant="ghost" onClick={() => ctx.removePaymentTerm(project.id, t.id)}>Reject</Button>
                </div>
              ) : editable ? (
                <Select value={t.status} onChange={e => ctx.setPaymentTermStatus(project.id, t.id, e.target.value)} className="!w-36 !py-1 !text-xs">
                  {['Not Due', 'Due', 'Requested', 'Paid'].map(s => <option key={s}>{s}</option>)}
                </Select>
              ) : <StatusBadge status={t.status} />}
              {editable && t.status !== 'Suggested' && <IconBtn title="Remove term" onClick={() => ctx.removePaymentTerm(project.id, t.id)}>✕</IconBtn>}
            </div>
          ))}
        </div>
      </Collapsible>

      <Collapsible title="Retainage">
        <div className="flex items-center gap-3 mb-2">
          <span className="text-sm text-[var(--leon-black)]/60">Project retainage rate:</span>
          {editable ? (
            <TextInput type="number" min="0" max="100" defaultValue={project.retainagePct} onBlur={e => ctx.setRetainagePct(project.id, Number(e.target.value))} className="!w-20 !py-1 !text-xs" />
          ) : <strong>{project.retainagePct}%</strong>}
          {editable && <span className="text-xs text-[var(--leon-black)]/40">%</span>}
        </div>
        <StatBox label="Total Retainage Held to Date" value={fmtMoney(totalRetainage)} />
      </Collapsible>

      <Collapsible title="Payment Requisitions" count={project.paymentRequisitions.length} right={editable && <Button size="sm" variant="ghost" onClick={() => setShowReq(true)}>+ Submit Requisition</Button>}>
        {project.paymentRequisitions.length === 0 ? <EmptyState text="None yet." /> : (
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1">Rev</th><th className="py-1">Type</th><th className="py-1">Ref</th><th className="py-1">Amount</th><th className="py-1">Retainage Held</th><th className="py-1">Date</th><th className="py-1">File</th><th className="py-1">Status</th><th className="py-1">Payment</th></tr></thead>
            <tbody>{project.paymentRequisitions.map(r => (
              <React.Fragment key={r.id}>
              <tr className="border-t border-[var(--leon-line)]">
                <td className="py-1.5 font-semibold cursor-pointer hover:underline" onClick={() => setReqDetailFor(r)}>R{r.revision}</td>
                <td className="py-1.5">{r.type}</td>
                <td className="py-1.5">{r.reference || '—'}</td>
                <td className={`py-1.5 font-semibold ${r.amount < 0 ? 'text-[var(--leon-red)]' : ''}`}>{fmtMoney(r.amount)}</td>
                <td className="py-1.5">{r.retainageHeld ? fmtMoney(r.retainageHeld) : '—'}</td>
                <td className="py-1.5">{fmtDate(r.date)}</td>
                <td className="py-1.5"><FileField name={r.file} url={r.fileUrl} onChange={(fname, url) => ctx.updatePaymentRequisition(project.id, r.id, { file: fname, fileUrl: url })} editable={editable} /></td>
                <td className="py-1.5">
                  {editable ? (
                    <Select value={r.status} onChange={e => ctx.setPaymentRequisitionStatus(project.id, r.id, e.target.value)} className="!py-0.5 !text-xs !w-32">
                      {['Submitted', 'Pending', 'Approved', 'Rejected', 'Revised'].map(s => <option key={s}>{s}</option>)}
                    </Select>
                  ) : <StatusBadge status={r.status} />}
                </td>
                {/* AIA requisitions carry the same receipt + bank-hold record
                    as a payment term, so the money reaches the calendar the
                    same way. */}
                <td className="py-1.5">
                  {r.receivedDate ? (
                    <span className="text-[var(--leon-green)] font-semibold whitespace-nowrap">
                      &#10003; Paid {fmtDate(r.receivedDate)}
                      {isBankHeld(r) && <span className="ml-1 text-[var(--leon-yellow)]">· held</span>}
                    </span>
                  ) : editable && ctx.canSeeAccountingHub ? (
                    <button onClick={() => setReceiptFor({ kind: 'requisition', record: r, label: `Requisition R${r.revision}`, amount: r.amount })}
                      className="text-[var(--leon-brown)] font-semibold whitespace-nowrap hover:underline">Record payment</button>
                  ) : <span className="text-[var(--leon-black)]/30">—</span>}
                </td>
              </tr>
              {isBankHeld(r) && (
                <tr><td colSpan={9} className="pb-2">
                  <BankHoldPanel ctx={ctx} projectId={project.id} kind="requisition" record={r} />
                </td></tr>
              )}
              </React.Fragment>
            ))}</tbody>
          </table>
        )}
      </Collapsible>

      {receiptFor && (
        <RecordClientReceiptModal
          open={!!receiptFor} onClose={() => setReceiptFor(null)} ctx={ctx}
          projectId={project.id} kind={receiptFor.kind} record={receiptFor.record}
          label={receiptFor.label} defaultAmount={receiptFor.amount} />
      )}

      <Collapsible title="Profitability">
        <div className="grid sm:grid-cols-2 gap-3 mb-3">
          <div className="border border-[var(--leon-line)] rounded-lg p-3"><p className="text-xs uppercase text-[var(--leon-black)]/50 font-semibold">Original Contract Value</p><p className="text-lg font-bold">{fmtMoney(prof.originalContractValue)}</p></div>
          <div className="border-2 border-[var(--leon-brown)] bg-[var(--leon-cream)] rounded-lg p-3"><p className="text-xs uppercase text-[var(--leon-brown)] font-semibold">Revised Contract Value</p><p className="text-lg font-bold">{fmtMoney(prof.revisedContractValue)}</p></div>
        </div>
        <div className="grid sm:grid-cols-3 gap-3">
          <StatBox label="Estimated Cost" value={fmtMoney(prof.estimatedCost)} />
          <StatBox label="Actual Cost to Date" value={fmtMoney(prof.actualCost)} />
          <StatBox label="Cost Variance" value={`${fmtMoney(prof.costVarianceAmt)} (${fmtPct(prof.costVariancePct)})`} tone={prof.costVarianceAmt > 0 ? 'red' : 'green'} />
          <StatBox label="Est. Margin %" value={fmtPct(prof.estMarginPct)} />
          <StatBox label="Actual Margin %" value={fmtPct(prof.actMarginPct)} />
          <StatBox label="Collection %" value={fmtPct(prof.collectionPct)} />
          <StatBox label="Payables Paid %" value={fmtPct(prof.payablesPaidPct)} />
        </div>
      </Collapsible>

      <AddPaymentRequisitionModal open={showReq} onClose={() => setShowReq(false)} ctx={ctx} project={project} />
      <AddPaymentTermModal open={showTerm} onClose={() => setShowTerm(false)} ctx={ctx} project={project} />
      <PaymentRequisitionDetailModal open={!!reqDetailFor} req={reqDetailFor} onClose={() => setReqDetailFor(null)} project={project} />
    </div>
  );
}
function PaymentRequisitionDetailModal({ open, req, onClose, project }) {
  if (!req) return null;
  return (
    <RecordDetailModal open={open} onClose={onClose} title={`Payment Requisition R${req.revision}`} printable
      fields={[
        { label: 'Type', value: req.type }, { label: 'Status', value: req.status },
        { label: 'Reference', value: req.reference }, { label: 'Amount', value: fmtMoney(req.amount) },
        { label: 'Retainage Held', value: req.retainageHeld ? fmtMoney(req.retainageHeld) : '—' }, { label: 'Date', value: fmtDate(req.date) },
        { label: 'Project', value: project.name }, { label: 'Notes', value: req.note },
      ]}
      attachments={req.fileUrl ? [{ name: req.file, url: req.fileUrl }] : []}
    />
  );
}
function AddPaymentTermModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ label: '', pct: '', trigger: '' });
  useEffect(() => { if (open) setForm({ label: '', pct: '', trigger: '' }); }, [open]);
  function submit() { if (!form.label.trim() || !form.pct) return; ctx.addPaymentTerm(project.id, { ...form, pct: Number(form.pct) }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Add Payment Term" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Term</Button></>}>
      <div className="space-y-3">
        <Field label="Label"><TextInput value={form.label} onChange={e => setForm({ ...form, label: e.target.value })} placeholder="e.g. Deposit" /></Field>
        <Field label="Percentage"><TextInput type="number" min="0" max="100" value={form.pct} onChange={e => setForm({ ...form, pct: e.target.value })} /></Field>
        <Field label="Trigger"><TextInput value={form.trigger} onChange={e => setForm({ ...form, trigger: e.target.value })} placeholder="e.g. Contract Signing" /></Field>
      </div>
    </Modal>
  );
}
function StatBox({ label, value, tone }) {
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3">
      <p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">{label}</p>
      <p className={`text-base font-bold ${tone === 'red' ? 'text-[var(--leon-red)]' : tone === 'green' ? 'text-[var(--leon-green)]' : ''}`}>{value}</p>
    </div>
  );
}
function AddPaymentRequisitionModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ type: 'Milestone', reference: '', amount: '', retainageHeld: '', date: todayISO(), file: '', fileUrl: null, note: '' });
  useEffect(() => { if (open) setForm({ type: 'Milestone', reference: '', amount: '', retainageHeld: '', date: todayISO(), file: '', fileUrl: null, note: '' }); }, [open]);
  function submit() { if (!form.amount) return; ctx.addPaymentRequisition(project.id, { ...form, amount: Number(form.amount) }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Submit Payment Requisition" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Submit</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Type"><Select value={form.type} onChange={e => setForm({ ...form, type: e.target.value })}>{['Milestone', 'Change Order', 'Back Charge'].map(t => <option key={t}>{t}</option>)}</Select></Field>
          <Field label="Reference" hint="e.g. Deposit, CO-1, BC-1"><TextInput value={form.reference} onChange={e => setForm({ ...form, reference: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Amount" hint={form.type === 'Back Charge' ? 'Enter as positive — stored as negative automatically.' : undefined}><TextInput type="number" min="0" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="Retainage Held (optional)"><TextInput type="number" min="0" value={form.retainageHeld} onChange={e => setForm({ ...form, retainageHeld: e.target.value })} /></Field>
        </div>
        <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
        <Field label="Attachment"><FileField name={form.file} url={form.fileUrl} onChange={(fname, url) => setForm({ ...form, file: fname, fileUrl: url })} editable /></Field>
        <Field label="Note"><TextArea rows={2} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

// ---- AIA Billing (Schedule of Values + Payment Applications) --------------
function BillingTab({ ctx, project }) {
  const editable = ctx.canEdit('billing');
  const [showCreateApp, setShowCreateApp] = useState(false);
  const [showSync, setShowSync] = useState(false);
  const [showHeaderDefaults, setShowHeaderDefaults] = useState(false);
  const [selectedAppId, setSelectedAppId] = useState(project.applications[project.applications.length - 1]?.id || null);

  useEffect(() => {
    if (!project.applications.find(a => a.id === selectedAppId)) {
      setSelectedAppId(project.applications[project.applications.length - 1]?.id || null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [project.applications.length]);

  const originalContract = project.sov.reduce((s, c) => s + c.items.reduce((s2, i) => s2 + i.scheduledValue, 0), 0);
  const selectedApp = project.applications.find(a => a.id === selectedAppId);
  const summary = selectedApp ? applicationSummary(project, selectedApp.id) : null;
  const syncPlan = planSovSyncFromContract(project);
  const syncHasChanges = syncPlan.toAdd.length > 0 || syncPlan.toUpdate.length > 0;

  return (
    <div>
      <p className="no-print text-xs text-[var(--leon-black)]/50 mb-2">G702/G703 progress billing — the Schedule of Values is derived from the executed contract and approved Change Orders, not typed from scratch.</p>

      <Collapsible
        title="Schedule of Values"
        count={project.sov.length}
        right={editable && (
          <div className="flex items-center gap-3">
            <button onClick={() => setShowHeaderDefaults(true)} className="text-xs text-[var(--leon-brown)] font-semibold">Header Defaults</button>
            <Button size="sm" onClick={() => setShowSync(true)}>{syncHasChanges ? `Sync from Contract (${syncPlan.toAdd.length + syncPlan.toUpdate.length})` : 'Sync from Contract'}</Button>
          </div>
        )}
      >
        <p className="text-xs text-[var(--leon-black)]/50 mb-2">SOV line total: <strong>{fmtMoney(originalContract)}</strong> · Executed Original Contract Value: <strong>{fmtMoney(project.originalContractValue)}</strong>{Math.abs(originalContract - project.originalContractValue) > 0.01 && <span className="text-[var(--leon-red)] font-semibold"> — out of sync, use Sync from Contract</span>}</p>
        {project.sov.length === 0 ? <EmptyState text="No schedule of values yet — use Sync from Contract to import it from the executed contract's scopes and approved Change Orders." /> : project.sov.map(cat => (
          <SovCategoryEditor key={cat.id} ctx={ctx} project={project} cat={cat} editable={editable} />
        ))}
      </Collapsible>

      <Collapsible title="Payment Applications" count={project.applications.length} right={editable && project.sov.length > 0 && <Button size="sm" variant="ghost" onClick={() => setShowCreateApp(true)}>{project.applications.length === 0 ? '+ Create First Application' : '+ Create Next Application'}</Button>}>
        {project.applications.length === 0 ? <EmptyState text="No applications yet — sync the Schedule of Values from the contract first, then create the first application." /> : (
          <div className="flex items-center gap-2 flex-wrap">
            {project.applications.map(a => (
              <button
                key={a.id}
                onClick={() => setSelectedAppId(a.id)}
                className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold border ${selectedAppId === a.id ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] hover:bg-[var(--leon-cream)]'}`}
              >
                App {a.number} <StatusBadge status={a.status} />
              </button>
            ))}
          </div>
        )}
      </Collapsible>

      {selectedApp && summary && <ApplicationDetail ctx={ctx} project={project} app={selectedApp} summary={summary} editable={editable} />}

      <CreateApplicationModal open={showCreateApp} onClose={() => setShowCreateApp(false)} ctx={ctx} project={project} />
      <SyncSovModal open={showSync} onClose={() => setShowSync(false)} ctx={ctx} project={project} plan={syncPlan} />
      <AiaHeaderDefaultsModal open={showHeaderDefaults} onClose={() => setShowHeaderDefaults(false)} ctx={ctx} project={project} />
    </div>
  );
}

function SyncSovModal({ open, onClose, ctx, project, plan }) {
  function apply() { ctx.applySovSync(project.id); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Sync Schedule of Values from Contract" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={apply} disabled={plan.toAdd.length === 0 && plan.toUpdate.length === 0}>Apply Sync</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Builds SOV lines from each scope's Sales Value and every approved Change Order. Nothing is applied until you confirm below — existing manually-entered lines are never touched.</p>
        {plan.toAdd.length === 0 && plan.toUpdate.length === 0 ? <EmptyState text="Already in sync — nothing to add or update." /> : (
          <>
            {plan.toAdd.length > 0 && (
              <div>
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1">New lines ({plan.toAdd.length})</p>
                <div className="space-y-1">
                  {plan.toAdd.map((a, i) => (
                    <div key={i} className="flex items-center justify-between text-xs border-b border-[var(--leon-line)] py-1"><span>{a.description}</span><span className="font-semibold">{fmtMoney(a.scheduledValue)}</span></div>
                  ))}
                </div>
              </div>
            )}
            {plan.toUpdate.length > 0 && (
              <div>
                <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 mb-1 mt-2">Lines to update ({plan.toUpdate.length})</p>
                <div className="space-y-1">
                  {plan.toUpdate.map((u, i) => (
                    <div key={i} className="flex items-center justify-between text-xs border-b border-[var(--leon-line)] py-1"><span>{u.description}</span><span className="font-semibold">{fmtMoney(u.item.scheduledValue)} → {fmtMoney(u.newValue)}</span></div>
                  ))}
                </div>
              </div>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}

function AiaHeaderDefaultsModal({ open, onClose, ctx, project }) {
  const blank = makeAiaHeaderDefaults();
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm({ ...blank, ...project.aiaHeaderDefaults }); }, [open]);
  function submit() { ctx.saveAiaHeaderDefaults(project.id, form); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="AIA Header Defaults" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Carried forward into every new Payment Application's header automatically. Owner/Architect are read from the Contacts tab instead.</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Construction Manager"><TextInput value={form.constructionManager} onChange={e => setForm({ ...form, constructionManager: e.target.value })} /></Field>
          <Field label="Work Cat No"><TextInput value={form.workCatNo} onChange={e => setForm({ ...form, workCatNo: e.target.value })} /></Field>
        </div>
        <p className="text-xs font-bold uppercase text-[var(--leon-black)]/50 pt-2">Contractor's Certification</p>
        <div className="grid grid-cols-2 gap-3">
          <Field label="State"><TextInput value={form.certState} onChange={e => setForm({ ...form, certState: e.target.value })} /></Field>
          <Field label="County"><TextInput value={form.certCounty} onChange={e => setForm({ ...form, certCounty: e.target.value })} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Notary Day" hint="e.g. 20"><TextInput value={form.notaryDay} onChange={e => setForm({ ...form, notaryDay: e.target.value })} /></Field>
          <Field label="Notary Month" hint="e.g. MAY"><TextInput value={form.notaryMonth} onChange={e => setForm({ ...form, notaryMonth: e.target.value })} /></Field>
        </div>
        <Field label="Notary Public Name"><TextInput value={form.notaryName} onChange={e => setForm({ ...form, notaryName: e.target.value })} /></Field>
        <Field label="Commission Expiration Date"><TextInput value={form.notaryExpiration} onChange={e => setForm({ ...form, notaryExpiration: e.target.value })} placeholder="e.g. MAR 06, 2031" /></Field>
      </div>
    </Modal>
  );
}

function SovCategoryEditor({ ctx, project, cat, editable }) {
  const [addingItem, setAddingItem] = useState(false);
  const [itemDesc, setItemDesc] = useState('');
  const [itemVal, setItemVal] = useState('');
  const catTotal = cat.items.reduce((s, i) => s + i.scheduledValue, 0);
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-3 mb-2">
      <div className="flex items-center justify-between mb-1.5">
        <p className="text-sm font-bold">{cat.name} <span className="font-normal text-[var(--leon-black)]/50">— {fmtMoney(catTotal)}</span></p>
        {editable && cat.items.length === 0 && <IconBtn title="Remove empty category" onClick={() => ctx.removeSovCategory(project.id, cat.id)}>✕</IconBtn>}
      </div>
      {cat.items.length > 0 && (
        <table className="w-full text-xs mb-1">
          <tbody>
            {cat.items.map(item => (
              <tr key={item.id} className="border-t border-[var(--leon-line)]">
                <td className="py-1 pr-2">
                  {editable ? <TextInput defaultValue={item.description} onBlur={e => ctx.updateSovItem(project.id, cat.id, item.id, { description: e.target.value })} className="!py-0.5 !text-xs" /> : item.description}
                  {item.sourceType && <span className="block text-[10px] text-[var(--leon-black)]/40">Linked to {item.sourceType === 'scope' ? 'scope' : 'Change Order'}</span>}
                </td>
                <td className="py-1 pr-2 w-32 text-right">{editable && !item.sourceType ? <TextInput type="number" defaultValue={item.scheduledValue} onBlur={e => ctx.updateSovItem(project.id, cat.id, item.id, { scheduledValue: Number(e.target.value) })} className="!py-0.5 !text-xs !text-right" /> : fmtMoney(item.scheduledValue)}</td>
                {editable && <td className="py-1 w-8"><IconBtn title="Remove" onClick={() => ctx.removeSovItem(project.id, cat.id, item.id)}>✕</IconBtn></td>}
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {editable && (
        addingItem ? (
          <div className="flex items-center gap-2 mt-2">
            <TextInput autoFocus value={itemDesc} onChange={e => setItemDesc(e.target.value)} placeholder="Line description" className="!flex-1 !py-1 !text-xs" />
            <TextInput type="number" value={itemVal} onChange={e => setItemVal(e.target.value)} placeholder="Scheduled value" className="!w-32 !py-1 !text-xs" />
            <Button size="sm" onClick={() => { if (itemDesc.trim() && itemVal) { ctx.addSovItem(project.id, cat.id, itemDesc.trim(), Number(itemVal)); setItemDesc(''); setItemVal(''); setAddingItem(false); } }}>Add</Button>
            <Button size="sm" variant="ghost" onClick={() => setAddingItem(false)}>✕</Button>
          </div>
        ) : <button onClick={() => setAddingItem(true)} className="text-[11px] text-[var(--leon-black)]/40 font-semibold mt-1">+ Add manual line (not linked to a scope or Change Order)</button>
      )}
    </div>
  );
}

function ApplicationDetail({ ctx, project, app, summary, editable }) {
  const [showPayment, setShowPayment] = useState(false);
  const [showCertify, setShowCertify] = useState(false);
  const [showReopen, setShowReopen] = useState(false);
  const [showVoid, setShowVoid] = useState(false);
  const [previewMode, setPreviewMode] = useState(false);
  const locked = AIA_LOCKED_STATUSES.includes(app.status);
  const canEditLines = editable && !locked;
  const validation = applicationValidation(summary, project);
  const existingAr = (project.paymentRequisitions || []).find(r => r.sourceApplicationId === app.id);
  const paidStates = ['Approved/Certified', 'Partially Paid', 'Paid'];

  return (
    <div>
      <div className="no-print flex items-center justify-between flex-wrap gap-2 mb-3 mt-2">
        <div>
          <h3 className="font-bold text-lg">Application {app.number} <StatusBadge status={app.status} /></h3>
          <p className="text-xs text-[var(--leon-black)]/50">Period ending {fmtDate(app.periodTo)} · Prepared by {app.preparedBy}</p>
        </div>
        <div className="flex items-center gap-2">
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
            <button onClick={() => setPreviewMode(false)} className={`px-3 py-1 rounded-md text-xs font-semibold ${!previewMode ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>Edit</button>
            <button onClick={() => setPreviewMode(true)} className={`px-3 py-1 rounded-md text-xs font-semibold ${previewMode ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>Document Preview</button>
          </div>
          <PrintButton onClick={() => window.print()} label="Print" />
        </div>
      </div>

      {app.status === 'Voided' && <div className="no-print bg-[var(--leon-red)]/10 border border-[var(--leon-red)] rounded-lg p-3 mb-3 text-sm"><strong>Voided</strong> — {app.voidReason}</div>}

      {previewMode ? (
        <div className="no-print border border-[var(--leon-line)] rounded-lg overflow-auto max-h-[80vh]">
          <div className="p-8 bg-white min-w-[1020px]">
            <AiaPrintPackage project={project} app={app} summary={summary} companyProfile={ctx.companyProfile} />
          </div>
        </div>
      ) : (
        <>
          <AiaHeaderFields ctx={ctx} project={project} app={app} editable={canEditLines} />
          <AiaValidationPanel validation={validation} />

          {canEditLines && (
            <label className="no-print flex items-center gap-1.5 text-xs mb-3">
              <input type="checkbox" checked={app.retainageReleased} onChange={e => ctx.setAppRetainageReleased(project.id, app.id, e.target.checked)} /> Release Retainage on this application
            </label>
          )}

          <div className="overflow-x-auto mb-4 no-print">
            <table className="w-full text-xs">
              <thead>
                <tr className="text-left text-[var(--leon-black)]/40 uppercase">
                  <th className="py-1 pr-2">Item</th><th className="py-1 pr-2">Scheduled (C)</th><th className="py-1 pr-2">Previous (D)</th>
                  <th className="py-1 pr-2">This Period (E)</th><th className="py-1 pr-2">Stored Incorp.</th><th className="py-1 pr-2">Stored Added</th>
                  <th className="py-1 pr-2">Stored Bal. (F)</th><th className="py-1 pr-2">Completed (G)</th>
                  <th className="py-1 pr-2">% (H)</th><th className="py-1 pr-2">Balance (I)</th><th className="py-1 pr-2">Retainage (J)</th>
                </tr>
              </thead>
              <tbody>
                {summary.lineResults.map(r => (
                  <BillingLineRow key={r.item.id} ctx={ctx} project={project} app={app} r={r} lineKey={r.item.id} isCoLine={false} editable={canEditLines} label={r.item.description} />
                ))}
                {summary.coLineResults.map(r => (
                  <BillingLineRow key={r.co.id} ctx={ctx} project={project} app={app} r={r} lineKey={r.co.id} isCoLine={true} editable={canEditLines} label={`${r.co.number} — ${r.co.description}`} />
                ))}
              </tbody>
            </table>
          </div>

          <div className="no-print grid sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
            <StatBox label="1. Original Contract Value" value={fmtMoney(summary.originalContract)} />
            <StatBox label="2. Net Change Orders" value={fmtMoney(summary.netChanges)} />
            <StatBox label="3. Contract Sum to Date" value={fmtMoney(summary.totalContract)} />
            <StatBox label="4. Completed &amp; Stored to Date" value={fmtMoney(summary.completedToDate)} />
            <StatBox label="5. Retainage" value={fmtMoney(summary.retainage)} />
            <StatBox label="6. Earned Less Retainage" value={fmtMoney(summary.completedLessRetainage)} />
            <StatBox label="7. Less Previous Certificates" value={fmtMoney(summary.previousApplications)} />
            <StatBox label="9. Balance to Finish" value={fmtMoney(summary.balanceToFinish)} />
          </div>
          <div className="no-print border-2 border-[var(--leon-brown)] bg-[var(--leon-cream)] rounded-lg p-4 mb-4">
            <p className="text-xs uppercase font-semibold text-[var(--leon-brown)]">8. Current Payment Due</p>
            <p className="text-2xl font-bold">{fmtMoney(summary.currentPaymentDue)}</p>
          </div>

          <div className="no-print flex items-center gap-2 flex-wrap mb-3">
            {canEditLines && app.status === 'Draft' && <Button size="sm" variant="ghost" onClick={() => ctx.markAiaApplicationStage(project.id, app.id, 'Ready for Review')}>Mark Ready for Review</Button>}
            {canEditLines && ['Draft', 'Ready for Review', 'Revised'].includes(app.status) && (
              <Button size="sm" onClick={() => ctx.markAiaApplicationStage(project.id, app.id, 'Submitted')} disabled={!validation.ok} title={!validation.ok ? 'Resolve blocking issues below first' : ''}>Submit</Button>
            )}
            {ctx.canApproveAiaApplication && app.status === 'Submitted' && <Button size="sm" onClick={() => setShowCertify(true)}>Certify</Button>}
            {ctx.canApproveAiaApplication && ['Submitted', 'Approved/Certified'].includes(app.status) && <Button size="sm" variant="ghost" onClick={() => setShowReopen(true)}>Reopen for Revision</Button>}
            {ctx.canApproveAiaApplication && !['Voided', 'Paid'].includes(app.status) && <Button size="sm" variant="danger" onClick={() => setShowVoid(true)}>Void</Button>}
          </div>

          {app.history && app.history.length > 0 && (
            <Collapsible title="Revision History" count={app.history.length}>
              <div className="space-y-1.5 text-xs">
                {app.history.map((h, i) => (
                  <div key={i} className="border-b border-[var(--leon-line)] pb-1.5 last:border-0"><strong>{fmtDate(h.date)}</strong> — {h.user}, reopened from {h.fromStatus}: {h.reason}</div>
                ))}
              </div>
            </Collapsible>
          )}

          <div className="no-print flex items-center gap-3 flex-wrap mt-2">
            <span className="text-sm font-semibold">Payment Status:</span>
            <StatusBadge status={app.payment.status} />
            {app.payment.status !== 'Unpaid' && <span className="text-xs text-[var(--leon-black)]/50">{fmtMoney(app.payment.amount)} · {app.payment.reference} · {fmtDate(app.payment.date)}</span>}
            {editable && paidStates.includes(app.status) && <Button size="sm" variant="ghost" onClick={() => setShowPayment(true)}>Update Payment Status</Button>}
            {editable && paidStates.includes(app.status) && !existingAr && <Button size="sm" variant="ghost" onClick={() => ctx.createArFromApplication(project.id, app.id, summary.currentPaymentDue)}>Create AR Record</Button>}
            {existingAr && <span className="text-xs text-[var(--leon-black)]/40">AR record on file ({existingAr.status}).</span>}
          </div>
        </>
      )}

      <div className="print-only print-area aia-print-package p-8">
        <AiaPrintPackage project={project} app={app} summary={summary} companyProfile={ctx.companyProfile} />
      </div>

      <SetPaymentModal open={showPayment} onClose={() => setShowPayment(false)} ctx={ctx} project={project} app={app} currentPaymentDue={summary.currentPaymentDue} />
      <CertifyApplicationModal open={showCertify} onClose={() => setShowCertify(false)} ctx={ctx} project={project} app={app} currentPaymentDue={summary.currentPaymentDue} />
      <ReopenApplicationModal open={showReopen} onClose={() => setShowReopen(false)} ctx={ctx} project={project} app={app} />
      <VoidApplicationModal open={showVoid} onClose={() => setShowVoid(false)} ctx={ctx} project={project} app={app} />
    </div>
  );
}

function AiaValidationPanel({ validation }) {
  if (validation.blocking.length === 0 && validation.warnings.length === 0) {
    return <div className="no-print text-xs text-[var(--leon-green)] font-semibold mb-3">✓ Passes all validation checks.</div>;
  }
  return (
    <div className="no-print mb-3 space-y-1.5">
      {validation.blocking.map((b, i) => <div key={`b${i}`} className="text-xs border border-[var(--leon-red)] bg-[var(--leon-red)]/10 text-[var(--leon-red)] rounded-lg px-3 py-1.5 font-semibold">⚠ {b}</div>)}
      {validation.warnings.map((w, i) => <div key={`w${i}`} className="text-xs border border-[var(--leon-yellow)] bg-[var(--leon-yellow)]/10 rounded-lg px-3 py-1.5">{w}</div>)}
    </div>
  );
}

function AiaHeaderFields({ ctx, project, app, editable }) {
  const h = app.header || {};
  function update(fields) { ctx.updateAiaApplicationHeader(project.id, app.id, fields); }
  return (
    <Collapsible title="Application Header">
      {/* This block prints on a real AIA application that goes to the owner and
          the architect. It is read far more often than it is changed, so it is
          locked until someone deliberately says otherwise. */}
      <EditLock canEdit={editable} hint="Locked — press Edit to change what prints on the application.">
      <div className="grid sm:grid-cols-3 gap-3 mb-3">
        <div><p className="text-xs text-[var(--leon-black)]/40 uppercase font-semibold mb-0.5">Owner</p><p className="text-sm font-semibold">{h.ownerCompany || '—'}{h.ownerPerson ? ` — ${h.ownerPerson}` : ''}</p></div>
        <div><p className="text-xs text-[var(--leon-black)]/40 uppercase font-semibold mb-0.5">Architect</p><p className="text-sm font-semibold">{h.architectCompany || '—'}{h.architectPerson ? ` — ${h.architectPerson}` : ''}</p></div>
        <div><p className="text-xs text-[var(--leon-black)]/40 uppercase font-semibold mb-0.5">Contractor</p><p className="text-sm font-semibold">{ctx.companyProfile.name}</p></div>
      </div>
      <div className="grid sm:grid-cols-3 gap-3">
        <Field label="Construction Manager">{editable ? <TextInput defaultValue={h.constructionManager} onBlur={e => update({ constructionManager: e.target.value })} /> : <p className="text-sm">{h.constructionManager || '—'}</p>}</Field>
        <Field label="Work Cat No">{editable ? <TextInput defaultValue={h.workCatNo} onBlur={e => update({ workCatNo: e.target.value })} /> : <p className="text-sm">{h.workCatNo || '—'}</p>}</Field>
        <Field label="Project Number"><p className="text-sm">{project.projectNumber}</p></Field>
        <Field label="Retainage %"><p className="text-sm">{project.retainagePct || 0}%</p></Field>
        <Field label="Contract Work"><p className="text-sm">{project.companyDepartment.join(' + ')}</p></Field>
      </div>
      <p className="text-xs font-bold uppercase text-[var(--leon-black)]/40 mt-3 mb-1.5">Contractor's Certification</p>
      <div className="grid sm:grid-cols-3 gap-3">
        <Field label="State">{editable ? <TextInput defaultValue={h.certState} onBlur={e => update({ certState: e.target.value })} /> : <p className="text-sm">{h.certState || '—'}</p>}</Field>
        <Field label="County">{editable ? <TextInput defaultValue={h.certCounty} onBlur={e => update({ certCounty: e.target.value })} /> : <p className="text-sm">{h.certCounty || '—'}</p>}</Field>
        <Field label="Notary Public Name">{editable ? <TextInput defaultValue={h.notaryName} onBlur={e => update({ notaryName: e.target.value })} /> : <p className="text-sm">{h.notaryName || '—'}</p>}</Field>
        <Field label="Notary Day">{editable ? <TextInput defaultValue={h.notaryDay} onBlur={e => update({ notaryDay: e.target.value })} /> : <p className="text-sm">{h.notaryDay || '—'}</p>}</Field>
        <Field label="Notary Month">{editable ? <TextInput defaultValue={h.notaryMonth} onBlur={e => update({ notaryMonth: e.target.value })} /> : <p className="text-sm">{h.notaryMonth || '—'}</p>}</Field>
        <Field label="Commission Expiration">{editable ? <TextInput defaultValue={h.notaryExpiration} onBlur={e => update({ notaryExpiration: e.target.value })} /> : <p className="text-sm">{h.notaryExpiration || '—'}</p>}</Field>
      </div>
      </EditLock>
    </Collapsible>
  );
}

// Shared by the on-screen Document Preview mode and the actual print output
// (rendered a second time inside .print-only so Print always produces this
// document regardless of which on-screen mode was left active). Rebuilt to
// match the client's own "Application for Payment" template pixel-for-
// pixel (§ AIA print template request) — a Summary page followed by a
// Payment Application Detail continuation sheet, category-grouped exactly
// like the reference document.
const AIA_NAVY = '#1a3a5e';
const AIA_LIGHT_BG = '#eef2f6';
const AIA_PINK_BG = '#fdecec';
const AIA_PINK_TEXT = '#9b2c2c';
// An explicit inline border (not a Tailwind utility class) so every cell's
// grid line survives print reliably — combined with the print-color-adjust
// rule in styles.css, this is what keeps columns visually separated on the
// actual printed/PDF page, not just in the on-screen preview.
const AIA_CELL_BORDER = '1px solid #8a8a8a';

function AiaPrintPackage({ project, app, summary, companyProfile }) {
  const h = app.header || {};
  const owner = project.contacts.Owner || {};
  const architect = project.contacts.Architect || {};
  const cp = companyProfile || { name: 'LEON Integra', addressLine1: '', addressLine2: '', country: 'US' };

  const categories = [];
  summary.lineResults.forEach(r => {
    let cat = categories.find(c => c.name === r.item.categoryName);
    if (!cat) { cat = { name: r.item.categoryName, rows: [] }; categories.push(cat); }
    cat.rows.push(r);
  });
  function sumRows(rows, key) { return rows.reduce((s, r) => s + r[key], 0); }
  function rowPct(rows) {
    const sched = sumRows(rows, 'scheduled');
    return sched !== 0 ? (sumRows(rows, 'completed') / sched) * 100 : 0;
  }
  const baseTotals = { scheduled: summary.sovLineTotal, previous: sumRows(summary.lineResults, 'previous'), workCurrent: sumRows(summary.lineResults, 'workCurrent'), stored: sumRows(summary.lineResults, 'stored'), completed: sumRows(summary.lineResults, 'completed'), balance: sumRows(summary.lineResults, 'balance'), retainage: sumRows(summary.lineResults, 'retainage') };
  const coTotals = { scheduled: sumRows(summary.coLineResults, 'scheduled'), previous: sumRows(summary.coLineResults, 'previous'), workCurrent: sumRows(summary.coLineResults, 'workCurrent'), stored: sumRows(summary.coLineResults, 'stored'), completed: sumRows(summary.coLineResults, 'completed'), balance: sumRows(summary.coLineResults, 'balance'), retainage: sumRows(summary.coLineResults, 'retainage') };
  const grandTotals = { scheduled: baseTotals.scheduled + coTotals.scheduled, previous: baseTotals.previous + coTotals.previous, workCurrent: baseTotals.workCurrent + coTotals.workCurrent, stored: baseTotals.stored + coTotals.stored, completed: baseTotals.completed + coTotals.completed, balance: baseTotals.balance + coTotals.balance, retainage: baseTotals.retainage + coTotals.retainage };
  const grandPct = grandTotals.scheduled !== 0 ? (grandTotals.completed / grandTotals.scheduled) * 100 : 0;

  // The label cell always spans the Item No + Description columns together
  // (colSpan=2) — a group/subtotal/grand-total row has no item number of
  // its own, so its label needs the full width those two columns share.
  // This was previously colSpan=1 (an unused "indent" prop left it always
  // false), which forced the narrow Item No column to blow up in width to
  // fit long labels like "TOTAL BASE CONTRACT WORK", throwing every other
  // row's columns out of alignment with each other.
  function DetailRow({ label, rows, bold, tone }) {
    return (
      <tr className={`aia-no-row-break ${bold ? 'font-bold' : ''}`} style={{ background: tone === 'pink' ? AIA_PINK_BG : bold ? AIA_LIGHT_BG : undefined, color: tone === 'pink' ? AIA_PINK_TEXT : undefined }}>
        <td className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER }} colSpan={2}>{label}</td>
        {rows}
      </tr>
    );
  }
  function numCell(v) { return <td className="py-1 px-1.5 text-right whitespace-nowrap" style={{ border: AIA_CELL_BORDER }}>{fmtAia(v)}</td>; }
  function pctCell(v) { return <td className="py-1 px-1.5 text-right whitespace-nowrap" style={{ border: AIA_CELL_BORDER }}>{fmtAiaPct(v)}</td>; }
  // Two distinct underlined fields side by side (a wide signature line, a
  // narrower date field) — not one shared line with the date crammed at the
  // far edge, which read as broken/unfinished rather than as a real
  // two-field signature block.
  function SignatureBlock({ leftLabel, dateValue }) {
    return (
      <>
        <div className="flex items-end gap-8 mt-2">
          <div className="flex-1 pb-0.5" style={{ borderBottom: '1px solid #666' }}>&nbsp;</div>
          <div className="pb-0.5 text-right" style={{ minWidth: '110px', borderBottom: '1px solid #666' }}>{dateValue}</div>
        </div>
        <div className="flex items-center gap-8 text-[10px] text-gray-500 mt-0.5">
          <div className="flex-1">{leftLabel}</div>
          <div className="text-right" style={{ minWidth: '110px' }}>Date</div>
        </div>
      </>
    );
  }
  function Blank({ value, chars }) {
    return <span style={{ borderBottom: '1px solid #999', display: 'inline-block', minWidth: `${chars}ch`, textAlign: 'center' }}>{value || ' '}</span>;
  }

  return (
    <div className="text-[13px] leading-tight" style={{ color: '#1a1a1a', fontFamily: 'Arial, Helvetica, sans-serif' }}>
      {/* ============ PAGE 1 — SUMMARY (must fit one page) ============ */}
      <div className="flex items-start justify-between pb-1.5 mb-1.5" style={{ borderBottom: `2px solid ${AIA_NAVY}` }}>
        <div>
          <img src="logo/leon-mark.svg" alt="LEON" className="h-[4.5rem] w-auto" />
          <p className="text-[9px] text-gray-500 leading-tight mt-0.5">{COMPANY_PRINT_ADDRESS}</p>
        </div>
        <div className="text-right">
          <p className="text-base font-bold leading-tight" style={{ color: AIA_NAVY }}>APPLICATION FOR PAYMENT</p>
          <p className="text-xs text-gray-500 leading-tight">Progress Billing — Summary</p>
        </div>
      </div>

      <div className="grid grid-cols-4 gap-x-6 gap-y-0.5 text-xs leading-tight mb-1.5 pb-1.5" style={{ borderBottom: '1px solid #ccc' }}>
        <div>
          <p className="font-bold leading-tight" style={{ color: AIA_NAVY }}>OWNER:</p>
          <p className="leading-tight">{h.ownerCompany || owner.company || '—'}</p>
        </div>
        <div>
          <p className="font-bold leading-tight" style={{ color: AIA_NAVY }}>PROJECT:</p>
          <p className="leading-tight">{project.name}</p>
          <p className="leading-tight">{project.address || ''}</p>
        </div>
        <div className="leading-tight">
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>APPLICATION NO: </span>{app.number}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>PERIOD TO: </span>{fmtDate(app.periodTo)}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>PREPARED BY: </span>{app.preparedBy}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>PROJECT NO: </span>{project.projectNumber}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>CONTRACT WORK: </span>{project.companyDepartment.join(' + ')}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>WORK CAT NO: </span>{h.workCatNo || '—'}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>ARCHITECT: </span>{h.architectCompany || architect.company || '—'}</p>
          <p><span className="font-bold" style={{ color: AIA_NAVY }}>RETAINAGE %: </span>{project.retainagePct || 0}</p>
        </div>
        <div className="leading-tight">
          <p className="font-bold" style={{ color: AIA_NAVY }}>DISTRIBUTION:</p>
          <p>Contractor ◄</p><p>Architect ◄</p><p>File ◄</p>
        </div>
        <div className="leading-tight">
          <p className="font-bold" style={{ color: AIA_NAVY }}>CONTRACTOR:</p>
          <p>{cp.name}</p>
          {cp.addressLine1 && <p>{cp.addressLine1}</p>}
          {cp.addressLine2 && <p>{cp.addressLine2}</p>}
        </div>
        <div className="leading-tight">
          <p className="font-bold" style={{ color: AIA_NAVY }}>CONSTRUCTION MANAGER:</p>
          <p>{h.constructionManager || ''}</p>
        </div>
      </div>

      <div className="grid grid-cols-2 gap-8">
        <div>
          <p className="text-sm font-bold leading-tight" style={{ color: AIA_NAVY }}>APPLICATION FOR PAYMENT — SUMMARY</p>
          <p className="text-[11px] italic text-gray-500 leading-tight mb-1">Refer to continuation sheet attached for detailed breakdown.</p>
          <table className="w-full text-xs">
            <tbody>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>1. ORIGINAL CONTRACT AMOUNT</td><td className="py-0.5 text-right">{fmtAia(summary.originalContract)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>2. NET CHANGES TO CONTRACT</td><td className="py-0.5 text-right">{fmtAia(summary.netChanges)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>3. TOTAL CONTRACT AMOUNT</td><td className="py-0.5 text-right">{fmtAia(summary.totalContract)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>4. TOTAL COMPLETED AND STORED TO DATE</td><td className="py-0.5 text-right">{fmtAia(summary.completedToDate)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}>
                <td className="py-0.5">
                  <p className="font-bold leading-tight" style={{ color: AIA_NAVY }}>5. RETAINAGE</p>
                  <p className="text-[10px] text-gray-400 leading-tight">{project.retainagePct || 0}% Retainage with adjustments</p>
                </td>
                <td className="py-0.5 text-right align-top">{fmtAia(summary.retainage)}</td>
              </tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>6. TOTAL COMPLETED LESS RETAINAGE</td><td className="py-0.5 text-right">{fmtAia(summary.completedLessRetainage)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>7. LESS PREVIOUS APPLICATIONS</td><td className="py-0.5 text-right">{fmtAia(summary.previousApplications)}</td></tr>
              <tr style={{ borderBottom: '1px solid #ddd' }}>
                <td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>8. CURRENT PAYMENT DUE</td>
                <td className="py-0.5 text-right font-bold" style={{ border: `1.5px solid ${AIA_NAVY}` }}>{fmtAia(summary.currentPaymentDue)}</td>
              </tr>
              <tr><td className="py-0.5 font-bold" style={{ color: AIA_NAVY }}>9. BALANCE TO FINISH INCLUDING RETAINAGE</td><td className="py-0.5 text-right">{fmtAia(summary.balanceToFinish)}</td></tr>
            </tbody>
          </table>

          <p className="text-xs font-bold mt-1.5 mb-1 leading-tight">EXTRA WORK SUMMARY <span className="font-normal text-gray-400">(auto from Change Orders section)</span></p>
          <table className="w-full text-xs" style={{ borderCollapse: 'collapse', border: '1px solid #ccc' }}>
            <thead>
              <tr style={{ background: AIA_LIGHT_BG }}>
                <th className="py-0.5 px-1.5 text-left" style={{ border: AIA_CELL_BORDER }}></th>
                <th className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>ADDITIONS</th>
                <th className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>DELETIONS</th>
              </tr>
            </thead>
            <tbody>
              <tr><td className="py-0.5 px-1.5" style={{ border: AIA_CELL_BORDER }}>Changes From Prev Applications</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.additionsPrev)}</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.deletionsPrev)}</td></tr>
              <tr><td className="py-0.5 px-1.5" style={{ border: AIA_CELL_BORDER }}>Changes From This Application</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.additionsThis)}</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.deletionsThis)}</td></tr>
              <tr className="font-bold"><td className="py-0.5 px-1.5" style={{ border: AIA_CELL_BORDER }}>Total</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.additionsTotal)}</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>{fmtAia(summary.extraWork.deletionsTotal)}</td></tr>
              <tr className="font-bold" style={{ background: AIA_LIGHT_BG }}><td className="py-0.5 px-1.5" style={{ border: AIA_CELL_BORDER }}>Net Changes</td><td className="py-0.5 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }} colSpan={2}>{fmtAia(summary.netChanges)}</td></tr>
            </tbody>
          </table>
        </div>

        <div className="text-[11px] leading-tight">
          <div className="border rounded" style={{ borderColor: '#ddd', padding: '10px 12px' }}>
            <p className="text-sm font-bold leading-tight" style={{ color: AIA_NAVY }}>CONTRACTOR'S CERTIFICATION:</p>
            <p className="mt-1 mb-1">The undersigned Contractor, to the best of his knowledge, information and belief, certifies that the Work covered by this Application for Payment has been completed in accordance with the Contract Documents, that all amounts have been paid for Work for which previous Certificates for Payment were issued and payments received from the Owner, and that current payment shown herein is now due.</p>

            <SignatureBlock leftLabel="Contractor" dateValue={fmtDate(app.periodTo)} />

            <p className="mt-2"><strong>State:</strong> <Blank value={h.certState} chars={6} /> &nbsp;&nbsp; <strong>County:</strong> <Blank value={h.certCounty} chars={10} /></p>
            <p className="mt-1.5">Subscribed &amp; sworn to before me this <Blank value={h.notaryDay} chars={4} /> day of <Blank value={h.notaryMonth} chars={8} />.</p>
            <p className="mt-1"><strong>Notary Public Name:</strong> <Blank value={h.notaryName} chars={20} /></p>
            <p className="mt-1"><strong>Commission Expiration Date:</strong> <Blank value={h.notaryExpiration} chars={14} /></p>
          </div>

          <div className="border rounded mt-2" style={{ borderColor: '#ddd', padding: '10px 12px' }}>
            <p className="text-sm font-bold leading-tight" style={{ color: AIA_NAVY }}>ARCHITECT'S CERTIFICATE FOR PAYMENT:</p>
            <p className="mt-1 mb-1">The Architect hereby confirms that, based on site observations and to the best of his/her knowledge, this payment application accurately reflects the progression of work and that this work meets contract requirements sufficient to justify payment in the amount certified below.</p>
            <p className="font-bold mt-1.5" style={{ color: AIA_NAVY }}>AMOUNT CERTIFIED: $ {app.certifiedAmount !== null && app.certifiedAmount !== undefined ? fmtAia(app.certifiedAmount) : ''}</p>
            <p className="mt-1 text-gray-500">Provide explanation if the amount certified does not match this application amount. Initial all figures &amp; markups to agree with the certified amount.</p>

            <SignatureBlock leftLabel="Architect" dateValue="mm/dd/yyyy" />

            <p className="mt-2 text-gray-500">The Amount Certified is payable to the contractor listed above.</p>
          </div>
        </div>
      </div>

      {/* ============ PAGE 2 — DETAIL ============ */}
      <div className="aia-page-break">
        <p className="text-center text-base font-bold mb-3" style={{ color: AIA_NAVY }}>PAYMENT APPLICATION DETAIL</p>
        <div className="flex items-start justify-between text-xs mb-3">
          <div className="space-y-0.5">
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>FROM: </span>{cp.name}</p>
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>WORK: </span>{project.companyDepartment.join(' + ')}</p>
          </div>
          <div className="space-y-0.5">
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>PROJECT: </span>{project.name}</p>
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>WORK CATEGORY NO: </span>{h.workCatNo || ''}</p>
          </div>
          <div className="text-xs" style={{ background: AIA_LIGHT_BG, padding: '4px 10px', borderRadius: 4 }}>
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>APPLICATION NO: </span>{app.number}</p>
            <p><span className="font-bold" style={{ color: AIA_NAVY }}>FOR PERIOD ENDING: </span>{app.periodTo}</p>
          </div>
        </div>

        <table className="w-full text-[10px]" style={{ borderCollapse: 'collapse', tableLayout: 'fixed' }}>
          <colgroup>
            <col style={{ width: '5%' }} />
            <col style={{ width: '21%' }} />
            <col style={{ width: '9%' }} />
            <col style={{ width: '8%' }} />
            <col style={{ width: '9%' }} />
            <col style={{ width: '8%' }} />
            <col style={{ width: '9%' }} />
            <col style={{ width: '5%' }} />
            <col style={{ width: '9%' }} />
            <col style={{ width: '9%' }} />
          </colgroup>
          <thead>
            <tr style={{ background: AIA_LIGHT_BG }}>
              <th rowSpan={2} className="py-1 px-1.5 text-left align-bottom" style={{ border: AIA_CELL_BORDER }}>ITEM<br/>NO.</th>
              <th rowSpan={2} className="py-1 px-1.5 text-left align-bottom" style={{ border: AIA_CELL_BORDER }}>DESCRIPTION</th>
              <th rowSpan={2} className="py-1 px-1.5 text-right align-bottom" style={{ border: AIA_CELL_BORDER }}>SCHEDULED<br/>VALUE</th>
              <th colSpan={5} className="py-1 px-1.5 text-center" style={{ border: AIA_CELL_BORDER }}>COMPLETED WORK</th>
              <th rowSpan={2} className="py-1 px-1.5 text-right align-bottom" style={{ border: AIA_CELL_BORDER }}>BALANCE<br/>TO FINISH</th>
              <th rowSpan={2} className="py-1 px-1.5 text-right align-bottom" style={{ border: AIA_CELL_BORDER }}>RETAINAGE</th>
            </tr>
            <tr style={{ background: AIA_LIGHT_BG }}>
              <th className="py-1 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>PREV. APP.</th>
              <th className="py-1 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>THIS APP.<br/>WORK IN PLACE</th>
              <th className="py-1 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>STORED<br/>MAT.</th>
              <th className="py-1 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>TOTAL VALUE</th>
              <th className="py-1 px-1.5 text-right" style={{ border: AIA_CELL_BORDER }}>%</th>
            </tr>
          </thead>
          <tbody>
            {categories.map((cat, ci) => (
              <React.Fragment key={cat.name}>
                <DetailRow label={<>▶ {ci + 1} {cat.name.toUpperCase()}</>} bold rows={[<td key="p" colSpan={8} className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER }}></td>]} />
                {cat.rows.map((r, ri) => (
                  <tr key={r.item.id} className="aia-no-row-break">
                    <td className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER }}>{ci + 1}.{ri + 1}</td>
                    <td className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER, wordBreak: 'break-word' }}>{r.item.description}</td>
                    {numCell(r.scheduled)}{numCell(r.previous)}{numCell(r.workCurrent)}{numCell(r.stored)}{numCell(r.completed)}{pctCell(r.pctComplete)}{numCell(r.balance)}{numCell(r.retainage)}
                  </tr>
                ))}
                <DetailRow label={`TOTAL OF ${ci + 1} ${cat.name.toUpperCase()}`} bold rows={[numCell(sumRows(cat.rows, 'scheduled')), numCell(sumRows(cat.rows, 'previous')), numCell(sumRows(cat.rows, 'workCurrent')), numCell(sumRows(cat.rows, 'stored')), numCell(sumRows(cat.rows, 'completed')), pctCell(rowPct(cat.rows)), numCell(sumRows(cat.rows, 'balance')), numCell(sumRows(cat.rows, 'retainage'))]} />
              </React.Fragment>
            ))}
            <DetailRow label="TOTAL BASE CONTRACT WORK" bold rows={[numCell(baseTotals.scheduled), numCell(baseTotals.previous), numCell(baseTotals.workCurrent), numCell(baseTotals.stored), numCell(baseTotals.completed), pctCell(baseTotals.scheduled !== 0 ? (baseTotals.completed / baseTotals.scheduled) * 100 : 0), numCell(baseTotals.balance), numCell(baseTotals.retainage)]} />

            {summary.coLineResults.length > 0 && (
              <>
                <DetailRow label="▶ EXTRAS / CHANGE ORDERS" bold tone="pink" rows={[<td key="p" colSpan={8} className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER }}></td>]} />
                {summary.coLineResults.map(r => (
                  <tr key={r.co.id} className="aia-no-row-break" style={{ background: AIA_PINK_BG, color: AIA_PINK_TEXT }}>
                    <td className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER }}>{r.co.number}</td>
                    <td className="py-1 px-1.5" style={{ border: AIA_CELL_BORDER, wordBreak: 'break-word' }}>{r.co.description}</td>
                    {numCell(r.scheduled)}{numCell(r.previous)}{numCell(r.workCurrent)}{numCell(r.stored)}{numCell(r.completed)}{pctCell(r.pctComplete)}{numCell(r.balance)}{numCell(r.retainage)}
                  </tr>
                ))}
                <DetailRow label="SUBTOTAL EXTRAS / CHANGE ORDERS" bold tone="pink" rows={[numCell(coTotals.scheduled), numCell(coTotals.previous), numCell(coTotals.workCurrent), numCell(coTotals.stored), numCell(coTotals.completed), pctCell(coTotals.scheduled !== 0 ? (coTotals.completed / coTotals.scheduled) * 100 : 0), numCell(coTotals.balance), numCell(coTotals.retainage)]} />
              </>
            )}

            <DetailRow label="GRAND TOTAL" bold rows={[numCell(grandTotals.scheduled), numCell(grandTotals.previous), numCell(grandTotals.workCurrent), numCell(grandTotals.stored), numCell(grandTotals.completed), pctCell(grandPct), numCell(grandTotals.balance), numCell(grandTotals.retainage)]} />
          </tbody>
        </table>
      </div>
    </div>
  );
}

function CertifyApplicationModal({ open, onClose, ctx, project, app, currentPaymentDue }) {
  const [amount, setAmount] = useState(currentPaymentDue);
  useEffect(() => { if (open) setAmount(currentPaymentDue); }, [open, currentPaymentDue]);
  function submit() { ctx.certifyApplication(project.id, app.id, Number(amount) || 0); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Certify Application ${app.number}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Certify</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Certifying locks this application's billed amounts and header. Further changes require Reopen for Revision.</p>
        <Field label="Amount Certified" hint={`Current Payment Due: ${fmtMoney(currentPaymentDue)}`}><TextInput type="number" value={amount} onChange={e => setAmount(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}
function ReopenApplicationModal({ open, onClose, ctx, project, app }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  function submit() { if (!reason.trim()) return; ctx.reopenApplication(project.id, app.id, reason.trim()); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Reopen Application ${app.number} for Revision`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit} disabled={!reason.trim()}>Reopen</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">The current billed amounts, header, and certified amount are snapshotted to Revision History before this application unlocks for editing.</p>
        <Field label="Reason for Reopening" hint="Required"><TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} autoFocus /></Field>
      </div>
    </Modal>
  );
}
function VoidApplicationModal({ open, onClose, ctx, project, app }) {
  const [reason, setReason] = useState('');
  useEffect(() => { if (open) setReason(''); }, [open]);
  function submit() { if (!reason.trim()) return; ctx.voidApplication(project.id, app.id, reason.trim()); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={`Void Application ${app.number}`} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button variant="danger" onClick={submit} disabled={!reason.trim()}>Void Application</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/50">Voiding permanently locks this application; it stays on file for the audit trail. Create a new application to replace it if needed.</p>
        <Field label="Reason for Voiding" hint="Required"><TextArea rows={3} value={reason} onChange={e => setReason(e.target.value)} autoFocus /></Field>
      </div>
    </Modal>
  );
}

function BillingLineRow({ ctx, project, app, r, lineKey, isCoLine, editable, label }) {
  const store = isCoLine ? app.coLines : app.lines;
  const line = store[lineKey] || { inputType: 'Amount', current: 0, percent: 0, formulaText: '', storedIncorporated: 0, storedAdded: 0, retainageOverride: null };
  const update = (fields) => isCoLine ? ctx.updateApplicationCoLine(project.id, app.id, lineKey, fields) : ctx.updateApplicationLine(project.id, app.id, lineKey, fields);
  return (
    <tr className="border-t border-[var(--leon-line)]">
      <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{label}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{fmtMoney(r.scheduled)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{fmtMoney(r.previous)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">
        {editable ? (
          <div className="flex items-center gap-1">
            <Select value={line.inputType} onChange={e => update({ inputType: e.target.value })} className="!py-0.5 !text-[10px] !w-16 !px-1">
              {CURRENT_INPUT_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
            </Select>
            {line.inputType === 'Amount' && <TextInput type="number" defaultValue={line.current} onBlur={e => update({ current: Number(e.target.value) })} className="!py-0.5 !text-xs !w-20" />}
            {line.inputType === 'Percent' && <TextInput type="number" defaultValue={line.percent} onBlur={e => update({ percent: Number(e.target.value) })} className="!py-0.5 !text-xs !w-16" />}
            {line.inputType === 'Formula' && <TextInput defaultValue={line.formulaText} onBlur={e => update({ formulaText: e.target.value })} placeholder="=S*50%-P" className="!py-0.5 !text-xs !w-28" />}
          </div>
        ) : fmtMoney(r.workCurrent)}
      </td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{editable ? <TextInput type="number" defaultValue={line.storedIncorporated} title={`Available to incorporate: ${fmtMoney(r.storedPreviousBalance)}`} onBlur={e => update({ storedIncorporated: Number(e.target.value) })} className="!py-0.5 !text-xs !w-20" /> : fmtMoney(r.storedIncorporated)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{editable ? <TextInput type="number" defaultValue={line.storedAdded} onBlur={e => update({ storedAdded: Number(e.target.value) })} className="!py-0.5 !text-xs !w-20" /> : fmtMoney(r.storedAdded)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{fmtMoney(r.stored)}</td>
      <td className="py-1.5 pr-2 font-semibold whitespace-nowrap">{fmtMoney(r.completed)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{fmtPct(r.pctComplete)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">{fmtMoney(r.balance)}</td>
      <td className="py-1.5 pr-2 whitespace-nowrap">
        {editable ? <TextInput type="number" defaultValue={line.retainageOverride ?? ''} placeholder={`${project.retainagePct}%`} onBlur={e => update({ retainageOverride: e.target.value === '' ? null : Number(e.target.value) })} className="!py-0.5 !text-xs !w-16" title="Override retainage % for this line" /> : fmtMoney(r.retainage)}
      </td>
    </tr>
  );
}

function CreateApplicationModal({ open, onClose, ctx, project }) {
  const [form, setForm] = useState({ periodTo: todayISO(), preparedBy: ctx.currentUserName });
  useEffect(() => { if (open) setForm({ periodTo: todayISO(), preparedBy: ctx.currentUserName }); }, [open]);
  function submit() { if (!form.periodTo) return; ctx.createApplication(project.id, form.periodTo, form.preparedBy); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title={project.applications.length === 0 ? 'Create First Application' : 'Create Next Application Cycle'} footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Create</Button></>}>
      <div className="space-y-3">
        <Field label="Period Ending"><TextInput type="date" value={form.periodTo} onChange={e => setForm({ ...form, periodTo: e.target.value })} /></Field>
        <Field label="Prepared By"><TextInput value={form.preparedBy} onChange={e => setForm({ ...form, preparedBy: e.target.value })} /></Field>
        {project.applications.length > 0 && <p className="text-xs text-[var(--leon-black)]/50">This rolls the latest application's current + stored work into "previous" for every line, resets current/stored, and resumes normal retainage (release does not carry forward).</p>}
      </div>
    </Modal>
  );
}

function SetPaymentModal({ open, onClose, ctx, project, app, currentPaymentDue }) {
  const [form, setForm] = useState({ status: 'Unpaid', amount: '', reference: '', date: todayISO() });
  useEffect(() => { if (open) setForm({ status: app.payment.status, amount: app.payment.amount || currentPaymentDue, reference: app.payment.reference || '', date: app.payment.date || todayISO() }); }, [open]);
  function submit() { ctx.setAppPayment(project.id, app.id, { status: form.status, amount: Number(form.amount) || 0, reference: form.reference, date: form.date }); onClose(); }
  return (
    <Modal open={open} onClose={onClose} title="Update Payment Status" footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save</Button></>}>
      <div className="space-y-3">
        <Field label="Status"><Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>{APPLICATION_PAYMENT_STATUSES.map(s => <option key={s}>{s}</option>)}</Select></Field>
        {form.status !== 'Unpaid' && (
          <>
            <Field label="Amount" hint={`Current payment due: ${fmtMoney(currentPaymentDue)}`}><TextInput type="number" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
            <Field label="Reference"><TextInput value={form.reference} onChange={e => setForm({ ...form, reference: e.target.value })} placeholder="e.g. Check #1042" /></Field>
            <Field label="Date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          </>
        )}
      </div>
    </Modal>
  );
}

// ---- Projected Profitability (§6-8, §10) — Accounting / Admin only --------
function ProfitabilityTab({ ctx, project }) {
  const editable = ctx.canEdit('profitability');
  const summary = projectProfitabilitySummary(project);
  const pl = projectPL(project);
  const unplanned = unplannedCostSummary(project);

  return (
    <div>
      <div className="mb-3"><Badge tone="black">🔒 Confidential — Accounting / Administration Only</Badge></div>

      <Collapsible title="Projected Profitability Summary">
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3">Scope</th><th className="py-1 pr-3">Sales Value</th><th className="py-1 pr-3">Projected Cost</th><th className="py-1 pr-3">Projected Profit</th><th className="py-1 pr-3">Projected Margin %</th><th className="py-1 pr-3"></th></tr></thead>
            <tbody>
              {summary.rows.map(r => (
                <tr key={r.scope.id} className="border-t border-[var(--leon-line)]">
                  <td className="py-1.5 pr-3 font-semibold">{r.scope.name}</td>
                  <td className="py-1.5 pr-3">{fmtMoney(r.scope.profitability.salesValue)}</td>
                  <td className="py-1.5 pr-3">{fmtMoney(r.totalProjectedCost)}</td>
                  <td className="py-1.5 pr-3">{fmtMoney(r.projectedProfit)}</td>
                  <td className="py-1.5 pr-3">{fmtPct(r.projectedMarginPct)}</td>
                  <td className="py-1.5 pr-3">{r.belowTarget && <Badge tone="red">Below Target — Review</Badge>}</td>
                </tr>
              ))}
              <tr className="border-t-2 border-[var(--leon-black)] font-bold">
                <td className="py-1.5 pr-3">Total</td>
                <td className="py-1.5 pr-3">{fmtMoney(summary.totalSales)}</td>
                <td className="py-1.5 pr-3">{fmtMoney(summary.totalCost)}</td>
                <td className="py-1.5 pr-3">{fmtMoney(summary.totalProfit)}</td>
                <td className="py-1.5 pr-3">{fmtPct(summary.overallMarginPct)}</td>
                <td></td>
              </tr>
            </tbody>
          </table>
        </div>
      </Collapsible>

      {project.scopes.map(scope => <ScopeProfitabilityBlock key={scope.id} ctx={ctx} project={project} scope={scope} editable={editable} />)}

      <Collapsible title="Project P&L">
        <div className="grid md:grid-cols-4 gap-3 mb-3">
          <StatBox label="Revenue (Contract + Approved COs)" value={fmtMoney(pl.revenue)} />
          <StatBox label="Budgeted Cost" value={fmtMoney(pl.budgetedCost)} />
          <StatBox label="Actual Cost" value={fmtMoney(pl.actualCost)} />
          <StatBox label="Variance" value={fmtMoney(pl.variance)} tone={pl.variance < 0 ? 'red' : 'green'} />
          <StatBox label="Projected Profit" value={fmtMoney(pl.projectedProfit)} />
          <StatBox label="Actual Profit" value={fmtMoney(pl.actualProfit)} />
          <StatBox label="Projected Margin %" value={fmtPct(pl.projectedMarginPct)} />
          <StatBox label="Actual Margin %" value={fmtPct(pl.actualMarginPct)} />
        </div>
        <div className="border border-[var(--leon-red)]/40 bg-[#fbe7e7] rounded-lg p-3 flex items-center justify-between">
          <div>
            <p className="text-xs font-bold uppercase text-[var(--leon-red)]">Unplanned Business Loss</p>
            <p className="text-[11px] text-[var(--leon-black)]/50">Total unplanned costs: {fmtMoney(pl.unplannedTotal)} · classified as Business Loss: <strong>{fmtMoney(pl.unplannedLoss)}</strong></p>
          </div>
        </div>
      </Collapsible>

      <Collapsible title="Unplanned Business Costs / Potential Loss" count={unplanned.items.length}>
        {unplanned.items.length === 0 ? <EmptyState text="No unplanned costs recorded." /> : (
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3">Vendor</th><th className="py-1 pr-3">Cause</th><th className="py-1 pr-3">Amount</th><th className="py-1 pr-3">Classification</th></tr></thead>
            <tbody>
              {unplanned.items.map(v => (
                <tr key={v.id} className="border-t border-[var(--leon-line)]">
                  <td className="py-1.5 pr-3 font-semibold">{v.vendorName}</td>
                  <td className="py-1.5 pr-3">{v.unplannedReason || v.category}</td>
                  <td className="py-1.5 pr-3">{fmtMoney(v.amount)}</td>
                  <td className="py-1.5 pr-3"><Badge tone={v.recoverability === 'Business Loss' ? 'red' : v.recoverability === 'Pending Determination' ? 'yellow' : 'green'}>{v.recoverability}</Badge></td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </Collapsible>
    </div>
  );
}
function CostBreakdownEditor({ costs, editable, onChange }) {
  return (
    <table className="w-full text-xs">
      <tbody>
        {PROFIT_COST_FIELDS.map(f => (
          <tr key={f.key} className="border-t border-[var(--leon-line)]">
            <td className="py-1 pr-2 text-[var(--leon-black)]/60">{f.label}</td>
            <td className="py-1 text-right w-28">
              {editable ? (
                <TextInput type="number" defaultValue={costs[f.key] || 0} onBlur={e => onChange(f.key, Number(e.target.value) || 0)} className="!py-0.5 !text-xs !text-right" />
              ) : fmtMoney(costs[f.key])}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
function ScopeProfitabilityBlock({ ctx, project, scope, editable }) {
  const calc = scopeProfitabilityCalc(scope);
  const variance = scopeVarianceRows(scope);
  const actualTotal = scopeActualTotalCost(scope);
  return (
    <Collapsible
      title={scope.name}
      right={calc.belowTarget && <Badge tone="red">BELOW TARGET MARGIN — REVIEW REQUIRED</Badge>}
    >
      <div className="grid md:grid-cols-2 gap-3 mb-3">
        <Field label="Sales / Contract Value">
          {editable ? <TextInput type="number" defaultValue={scope.profitability.salesValue} onBlur={e => ctx.updateScopeProfitability(project.id, scope.id, { salesValue: Number(e.target.value) || 0 })} /> : <p className="font-semibold">{fmtMoney(scope.profitability.salesValue)}</p>}
        </Field>
        <Field label="Target Margin %">
          {editable ? <TextInput type="number" defaultValue={scope.profitability.targetMarginPct} onBlur={e => ctx.updateScopeProfitability(project.id, scope.id, { targetMarginPct: Number(e.target.value) || 0 })} /> : <p className="font-semibold">{fmtPct(scope.profitability.targetMarginPct)}</p>}
        </Field>
      </div>
      <div className="grid md:grid-cols-2 gap-4">
        <div>
          <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">Projected Cost</p>
          <CostBreakdownEditor costs={scope.profitability.costs} editable={editable} onChange={(key, val) => ctx.updateScopeCostField(project.id, scope.id, 'costs', key, val)} />
          <div className="flex justify-between text-xs font-bold border-t-2 border-[var(--leon-black)] pt-1 mt-1">
            <span>Projected Total Cost</span><span>{fmtMoney(calc.totalProjectedCost)}</span>
          </div>
          <div className="flex justify-between text-xs mt-1"><span>Projected Profit</span><span className="font-semibold">{fmtMoney(calc.projectedProfit)}</span></div>
          <div className="flex justify-between text-xs"><span>Projected Margin %</span><span className="font-semibold">{fmtPct(calc.projectedMarginPct)}</span></div>
        </div>
        <div>
          <div className="flex items-center justify-between mb-1.5">
            <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50">Actual Cost</p>
            {editable && (
              <button
                onClick={() => ctx.syncScopeActualCostsFromSystem(project.id, scope.id)}
                title="Recomputes Vendor Cost, Ocean/Domestic Freight, Warehousing, and Tariffs/Duties from Proforma Invoices, Export container costs, and Trade Compliance tariff lines. A field only changes if matching records exist — nothing is zeroed out just because none were found. Installation, Overhead, and Other stay manual — nothing in the system tracks their actual cost. Any field can still be edited by hand afterward."
                className="text-[11px] text-[var(--leon-brown)] font-semibold"
              >
                🔄 Sync from System
              </button>
            )}
          </div>
          <CostBreakdownEditor costs={scope.profitability.actual} editable={editable} onChange={(key, val) => ctx.updateScopeCostField(project.id, scope.id, 'actual', key, val)} />
          <div className="flex justify-between text-xs font-bold border-t-2 border-[var(--leon-black)] pt-1 mt-1">
            <span>Actual Total Cost</span><span>{fmtMoney(actualTotal)}</span>
          </div>
          <p className="text-[10px] text-[var(--leon-black)]/40 mt-1">Vendor Cost reference (approved estimates on file for this scope): {fmtMoney((project.vendorEstimates || []).filter(v => v.scopeId === scope.id && v.pmApproved && v.ownerApproved).reduce((s, v) => s + v.amount, 0))}</p>
        </div>
      </div>

      {!scope.profitability.baseline ? (
        editable && (
          <div className="mt-3 flex justify-end">
            <Button size="sm" onClick={() => ctx.lockScopeBaseline(project.id, scope.id)}>Lock Original Profitability Baseline</Button>
          </div>
        )
      ) : (
        <div className="mt-3 pt-3 border-t border-[var(--leon-line)]">
          <p className="text-xs text-[var(--leon-black)]/50 mb-1.5">Original Profitability Baseline locked {fmtDate(scope.profitability.baseline.lockedDate)} by {scope.profitability.baseline.lockedBy}.</p>
          <table className="w-full text-xs">
            <thead><tr className="text-left text-[var(--leon-black)]/40 uppercase"><th className="py-1 pr-3"></th><th className="py-1 pr-3">Projected</th><th className="py-1 pr-3">Actual</th><th className="py-1 pr-3">Variance</th></tr></thead>
            <tbody>
              {variance.map(row => {
                // For Profit/Margin %, higher actual is good (green). For every
                // cost line above them, higher actual (a cost overrun) is bad —
                // so the good/bad direction is inverted for those rows.
                const isProfitLike = row.label === 'Profit' || row.label === 'Margin %';
                const good = isProfitLike ? row.variance >= 0 : row.variance <= 0;
                return (
                  <tr key={row.label} className="border-t border-[var(--leon-line)]">
                    <td className="py-1 pr-3 font-semibold">{row.label}</td>
                    <td className="py-1 pr-3">{row.isPct ? fmtPct(row.baseline) : fmtMoney(row.baseline)}</td>
                    <td className="py-1 pr-3">{row.isPct ? fmtPct(row.actual) : fmtMoney(row.actual)}</td>
                    <td className={`py-1 pr-3 font-semibold ${good ? 'text-[#3a7d44]' : 'text-[var(--leon-red)]'}`}>{row.isPct ? fmtPct(row.variance) : fmtMoney(row.variance)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </Collapsible>
  );
}

// ---- Reports -------------------------------------------------------------

// ---- Change Log -------------------------------------------------------------
function ChangeLogTab({ ctx, project }) {
  const [note, setNote] = useState('');
  return (
    <div>
      <div className="flex items-center gap-2 mb-3">
        <TextInput value={note} onChange={e => setNote(e.target.value)} placeholder="Add a manual note…" className="flex-1" />
        <Button onClick={() => { if (note.trim()) { ctx.addManualLog(project.id, note.trim()); setNote(''); } }}>Add Note</Button>
      </div>
      <div className="space-y-1.5">
        {project.changeLog.map(l => (
          <div key={l.id} className="flex items-start gap-2 border-b border-[var(--leon-line)] py-1.5 text-sm">
            <span className="text-xs text-[var(--leon-black)]/40 w-20 shrink-0">{fmtDate(l.date)}</span>
            <Avatar name={l.user} size={22} />
            <span><strong>{l.user}</strong> <span className="text-[var(--leon-black)]/40 text-xs">({l.role})</span> — {l.action}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ============================================================================
// Admin Settings — Scope Library (§8.2)
// ============================================================================
// LEON's own company name/address, used as the "Contractor" block on the
// AIA Application for Payment print package (§ AIA print template request)
// — one shared record, editable here rather than duplicated per project.
// ============================================================================
// Company Setup — LEON's own record (LEON Library -> Company Setup)
// ----------------------------------------------------------------------------
// Everyone can read it: this is where the team looks up our address, our
// licence numbers, and who to send a certificate of insurance to. Only
// Accounting and Admin can change it, and the remittance/tax block is only
// shown to them at all.
// ============================================================================
const COMPANY_SETUP_SECTIONS = [
  {
    key: 'identity', title: 'Identity', note: 'How LEON is named on contracts and on everything we send out.',
    fields: [
      { key: 'name', label: 'Legal Name', hint: 'As it appears on contracts' },
      { key: 'tradeName', label: 'Trade Name (DBA)' },
      { key: 'entityType', label: 'Entity Type', hint: 'LLC, Corp, etc.' },
      { key: 'foundedYear', label: 'Founded' },
      { key: 'tagline', label: 'Tagline', full: true },
      { key: 'about', label: 'About LEON', type: 'textarea', full: true, hint: 'Shown to the whole team' },
    ],
  },
  {
    key: 'contact', title: 'Contact & Main Office',
    fields: [
      { key: 'phone', label: 'Phone' },
      { key: 'email', label: 'General Email' },
      { key: 'website', label: 'Website' },
      { key: 'country', label: 'Country' },
      { key: 'addressLine1', label: 'Address Line 1' },
      { key: 'addressLine2', label: 'City, State ZIP' },
    ],
  },
  {
    key: 'warehouse', title: 'Warehouse / Shop', note: 'Where material is received, if different from the office.',
    fields: [
      { key: 'warehouseName', label: 'Location Name' },
      { key: 'warehouseAddressLine1', label: 'Address Line 1' },
      { key: 'warehouseAddressLine2', label: 'City, State ZIP' },
    ],
  },
  {
    key: 'insurance', title: 'Insurance', note: 'The numbers a general contractor asks for before we mobilize.',
    fields: [
      { key: 'glCarrier', label: 'General Liability — Carrier' },
      { key: 'glPolicyNumber', label: 'GL Policy Number' },
      { key: 'glExpiry', label: 'GL Expires', type: 'date' },
      { key: 'wcCarrier', label: "Workers' Comp — Carrier" },
      { key: 'wcPolicyNumber', label: 'WC Policy Number' },
      { key: 'wcExpiry', label: 'WC Expires', type: 'date' },
    ],
  },
  {
    key: 'legal', title: 'Legal & Registration',
    fields: [
      { key: 'ein', label: 'Tax ID / EIN' },
      { key: 'stateOfIncorporation', label: 'State of Incorporation' },
      { key: 'duns', label: 'DUNS' },
      { key: 'licenses', label: 'Licenses & Registrations', type: 'textarea', full: true, hint: 'One per line' },
    ],
  },
  {
    key: 'remittance', title: 'Remittance', restricted: true, note: 'How clients pay us. Accounting and Admin only.',
    fields: [
      { key: 'bankName', label: 'Bank' },
      { key: 'bankAccountName', label: 'Account Name' },
      { key: 'bankAccountLast4', label: 'Account (last 4)' },
      { key: 'bankRoutingLast4', label: 'Routing (last 4)' },
      { key: 'remittanceEmail', label: 'Remittance Advice To' },
      { key: 'paymentInstructions', label: 'Payment Instructions', type: 'textarea', full: true },
    ],
  },
];

// The company profile reads as a PRESENTATION PAGE — the page you would hand
// someone, or print as the front of a catalog — not as a settings screen.
// It used to be a stack of collapsible categories, which is the right shape for
// editing a record and the wrong shape for reading one: the whole point of a
// company profile is that it is seen at once.
//
// Everyone reads it. Only Admin edits, and editing is a deliberate mode switch
// rather than fields that are always live, so nobody changes the company's
// legal name by tabbing through a page they opened to look something up.
function CompanySetupView({ ctx }) {
  const canEdit = ctx.currentRole === 'Admin';
  // Remittance is the one block that stays behind a gate: bank details and
  // payment instructions are what an invoice-fraud attempt needs, and a
  // company profile is not the place to publish them to everyone.
  const canSeeRemittance = ctx.canSeeAccountingHub;
  const [editing, setEditing] = useState(false);
  const [form, setForm] = useState(ctx.companyProfile);
  useEffect(() => { setForm(ctx.companyProfile); }, [ctx.companyProfile]);
  function save(fields) { ctx.updateCompanyProfile(fields || form); }
  const p = ctx.companyProfile;

  const expiries = [
    { label: 'General Liability', date: p.glExpiry },
    { label: "Workers' Comp", date: p.wcExpiry },
  ].filter(x => x.date).map(x => ({ ...x, days: daysBetween(todayISO(), x.date) }));
  const expiring = expiries.filter(x => x.days <= 45);

  // ---- edit mode: the record, field by field ----
  if (editing) {
    return (
      <div>
        <div className="flex items-center gap-2 flex-wrap mb-4">
          <div className="flex-1 min-w-0">
            <p className="text-sm text-[var(--leon-black)]/50 max-w-2xl">
              Editing the company record. Changes save as you leave each field and take effect
              everywhere immediately &mdash; on emails, printed documents and AIA applications.
            </p>
          </div>
          <Button onClick={() => setEditing(false)}>Done</Button>
        </div>
        <div className="border border-[var(--leon-line)] rounded-xl bg-white p-4 mb-4 flex items-center gap-4 flex-wrap">
          {p.logoUrl
            ? <ClickableImage src={p.logoUrl} name="Company icon" alt="Company icon" className="w-16 h-16 object-contain rounded-lg border border-[var(--leon-line)] bg-white p-1" />
            : <Avatar name={p.tradeName || p.name} url={null} size={64} />}
          <div>
            <p className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">Company icon</p>
            <div className="flex items-center gap-2 mt-1">
              <ImagePicker url={null} size={26} onChange={url => save({ ...form, logoUrl: url })} />
              <span className="text-xs font-semibold text-[var(--leon-brown)]">{p.logoUrl ? 'Replace' : 'Add icon'}</span>
              {p.logoUrl && (
                <button onClick={() => { if (confirm('Remove the company icon?')) save({ ...form, logoUrl: null }); }}
                  className="text-xs font-semibold text-[var(--leon-red)] hover:underline">Remove</button>
              )}
            </div>
          </div>
        </div>
        {COMPANY_SETUP_SECTIONS.map(sec => {
          if (sec.restricted && !canSeeRemittance) return null;
          return (
            <Collapsible key={sec.key} title={sec.title} defaultOpen
              right={sec.restricted && <Badge tone="neutral">Accounting &amp; Admin only</Badge>}>
              {sec.note && <p className="text-xs text-[var(--leon-black)]/45 mb-2">{sec.note}</p>}
              <div className="grid sm:grid-cols-2 gap-3">
                {sec.fields.map(f => (
                  <div key={f.key} className={f.full ? 'sm:col-span-2' : ''}>
                    <Field label={f.label} hint={f.hint}>
                      {f.type === 'textarea'
                        ? <TextArea rows={f.key === 'about' ? 4 : 3} value={form[f.key] || ''}
                            onChange={e => setForm({ ...form, [f.key]: e.target.value })} onBlur={() => save()} />
                        : <TextInput type={f.type || 'text'} value={form[f.key] || ''}
                            onChange={e => setForm({ ...form, [f.key]: e.target.value })} onBlur={() => save()} />}
                    </Field>
                  </div>
                ))}
              </div>
            </Collapsible>
          );
        })}
        <CompanyOfficesEditor ctx={ctx} />
        <p className="text-[11px] text-[var(--leon-black)]/40 mt-3">
          The legal name and address here print as &ldquo;Contractor&rdquo; on AIA Applications for Payment.
        </p>
      </div>
    );
  }

  // ---- presentation mode: everyone ----
  const office = [p.addressLine1, p.addressLine2, p.country].filter(Boolean);
  const shop = [p.warehouseAddressLine1, p.warehouseAddressLine2].filter(Boolean);
  const licenses = String(p.licenses || '').split('\n').map(x => x.trim()).filter(Boolean);
  const offices = (p.offices || []).filter(o => o.active !== false);
  const facts = [
    { label: 'Entity', value: p.entityType },
    { label: 'Founded', value: p.foundedYear },
    { label: 'State of Incorporation', value: p.stateOfIncorporation },
    { label: 'Tax ID / EIN', value: p.ein },
    { label: 'DUNS', value: p.duns },
  ].filter(x => x.value);

  return (
    <div>
      <div className="flex items-center justify-end gap-2 mb-3 no-print">
        {canEdit
          ? <Button variant="outline" onClick={() => setEditing(true)}>&#9998; Edit company profile</Button>
          : <span className="text-xs text-[var(--leon-black)]/40">Maintained by Admin</span>}
      </div>

      {/* Masthead. The brand lockup and the name, the way the front of a
          catalog carries it — not a form header. */}
      <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden mb-4">
        <div className="bg-[var(--leon-black)] px-6 py-7 flex items-center gap-5 flex-wrap">
          {p.logoUrl
            ? <ClickableImage src={p.logoUrl} name="Company icon" alt="" className="w-20 h-20 object-contain rounded-lg bg-white p-1.5" />
            : <img src="logo/leon-wordmark.svg" alt="LEON" className="h-9 w-auto invert" />}
          <div className="min-w-0">
            <h1 className="text-2xl font-bold text-white leading-tight">{p.tradeName || p.name}</h1>
            {p.tagline && <p className="text-sm text-[var(--leon-brown-light)] mt-1">{p.tagline}</p>}
            {p.tradeName && p.name && p.name !== p.tradeName && (
              <p className="text-xs text-white/45 mt-1.5">{p.name}{p.entityType ? ` · ${p.entityType}` : ''}</p>
            )}
          </div>
        </div>
        {p.about && (
          <div className="px-6 py-5 border-b border-[var(--leon-line)]">
            <p className="text-[15px] leading-relaxed text-[var(--leon-black)]/75 whitespace-pre-wrap max-w-3xl">{p.about}</p>
          </div>
        )}
        {/* Everything at once, side by side — no sections to open. */}
        <div className="grid md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-[var(--leon-line)]">
          <CompanyBlock title="Head Office" lines={office}
            contact={[p.phone, p.email, p.website].filter(Boolean)} />
          <CompanyBlock title="Warehouse / Shop" lines={shop.length ? shop : null}
            sub={p.warehouseName} empty="Same as the head office." />
          <CompanyBlock title="Registration" facts={facts} />
        </div>
        {offices.length > 0 && (
          <div className="border-t border-[var(--leon-line)]">
            <p className="px-5 pt-4 pb-1 text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">
              Other Locations
            </p>
            <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-x-5 gap-y-3 px-5 pb-4">
              {offices.map(o => (
                <div key={o.id}>
                  <p className="text-sm font-semibold">{o.name || o.type}
                    {o.name && o.type ? <span className="text-[var(--leon-black)]/40 font-normal"> · {o.type}</span> : null}</p>
                  {[o.addressLine1, o.addressLine2, o.country].filter(Boolean).map((l, i) =>
                    <p key={i} className="text-sm text-[var(--leon-black)]/70">{l}</p>)}
                  {[o.contactName, o.phone, o.email].filter(Boolean).length > 0 && (
                    <p className="text-xs text-[var(--leon-black)]/50 mt-0.5">
                      {[o.contactName, o.phone, o.email].filter(Boolean).join(' · ')}
                    </p>
                  )}
                </div>
              ))}
            </div>
          </div>
        )}
      </div>

      {expiring.length > 0 && (
        <div className="mb-4 px-3 py-2 rounded-lg border border-[var(--leon-yellow)]/60 bg-[var(--leon-yellow)]/10 text-sm">
          {expiring.map(x => (
            <p key={x.label} className={x.days < 0 ? 'text-[var(--leon-red)] font-semibold' : 'text-[var(--leon-yellow)] font-semibold'}>
              {x.days < 0 ? '!' : '⚠'} {x.label} certificate {x.days < 0 ? `expired ${Math.abs(x.days)} days ago` : `expires in ${x.days} days`} ({fmtDate(x.date)}).
            </p>
          ))}
        </div>
      )}

      <div className="grid md:grid-cols-2 gap-4">
        {/* Insurance — the numbers a general contractor asks for before we
            mobilise, which is exactly why the whole team can see them. */}
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
          <p className="px-4 py-2 bg-[var(--leon-cream)] text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/50">Insurance</p>
          <div className="divide-y divide-[var(--leon-line)]">
            {[
              { t: 'General Liability', carrier: p.glCarrier, policy: p.glPolicyNumber, exp: p.glExpiry },
              { t: "Workers' Compensation", carrier: p.wcCarrier, policy: p.wcPolicyNumber, exp: p.wcExpiry },
            ].map(x => (
              <div key={x.t} className="px-4 py-3">
                <p className="text-sm font-bold">{x.t}</p>
                {x.carrier || x.policy || x.exp ? (
                  <p className="text-xs text-[var(--leon-black)]/55 mt-0.5">
                    {[x.carrier, x.policy ? `Policy ${x.policy}` : null, x.exp ? `Expires ${fmtDate(x.exp)}` : null].filter(Boolean).join(' · ')}
                  </p>
                ) : <p className="text-xs text-[var(--leon-black)]/35 italic mt-0.5">Not on file.</p>}
              </div>
            ))}
          </div>
        </div>

        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
          <p className="px-4 py-2 bg-[var(--leon-cream)] text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/50">Licenses &amp; Registrations</p>
          {licenses.length === 0
            ? <p className="px-4 py-3 text-xs text-[var(--leon-black)]/35 italic">None on file.</p>
            : <ul className="divide-y divide-[var(--leon-line)]">
                {licenses.map((l, i) => <li key={i} className="px-4 py-2 text-sm">{l}</li>)}
              </ul>}
        </div>
      </div>

      {canSeeRemittance && (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden mt-4">
          <p className="px-4 py-2 bg-[var(--leon-cream)] flex items-center gap-2 text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/50">
            Remittance <Badge tone="neutral">Accounting &amp; Admin only</Badge>
          </p>
          <div className="px-4 py-3 grid sm:grid-cols-2 gap-x-6 gap-y-2">
            {[
              ['Bank', p.bankName], ['Account Name', p.bankAccountName],
              ['Account (last 4)', p.bankAccountLast4], ['Routing (last 4)', p.bankRoutingLast4],
              ['Remittance Advice To', p.remittanceEmail],
            ].filter(x => x[1]).map(([l, v]) => (
              <p key={l} className="text-sm"><span className="text-[var(--leon-black)]/45">{l}</span> &middot; <b>{v}</b></p>
            ))}
          </div>
          {p.paymentInstructions && <p className="px-4 pb-3 text-sm whitespace-pre-wrap">{p.paymentInstructions}</p>}
        </div>
      )}

      <p className="text-[11px] text-[var(--leon-black)]/40 mt-4">
        The legal name and address here print as &ldquo;Contractor&rdquo; on AIA Applications for Payment,
        and sign every email the Hub sends.
      </p>
    </div>
  );
}
// Every other location the company works out of. A list, not more fixed
// fields — how many offices there are is not the schema's business.
function CompanyOfficesEditor({ ctx }) {
  const offices = (ctx.companyProfile.offices || []).filter(o => o.active !== false);
  const editable = ctx.currentRole === 'Admin';
  return (
    <Collapsible title="Other Locations" count={offices.length} defaultOpen
      right={<Button size="sm" variant="ghost" onClick={() => ctx.addCompanyOffice({})}>+ Add Location</Button>}>
      <p className="text-xs text-[var(--leon-black)]/45 mb-2">
        Showrooms, satellite offices, factories &mdash; anywhere the company works from besides the
        head office and the warehouse above.
      </p>
      {offices.length === 0 ? <EmptyState text="No other locations yet." /> : (
        <EditLock canEdit={editable} hint="Locked — press Edit to change these.">
        <div className="space-y-3">
          {offices.map(o => (
            <div key={o.id} className="border border-[var(--leon-line)] rounded-lg p-3">
              <div className="grid sm:grid-cols-3 gap-3">
                <Field label="Location name"><TextInput value={o.name} onChange={e => ctx.updateCompanyOffice(o.id, { name: e.target.value })} placeholder="e.g. Miami Showroom" /></Field>
                <Field label="Type">
                  <Select value={o.type} onChange={e => ctx.updateCompanyOffice(o.id, { type: e.target.value })}>
                    {OFFICE_TYPES.map(t => <option key={t}>{t}</option>)}
                  </Select>
                </Field>
                <Field label="Country"><TextInput value={o.country} onChange={e => ctx.updateCompanyOffice(o.id, { country: e.target.value })} /></Field>
                <Field label="Address Line 1"><TextInput value={o.addressLine1} onChange={e => ctx.updateCompanyOffice(o.id, { addressLine1: e.target.value })} /></Field>
                <Field label="City, State ZIP"><TextInput value={o.addressLine2} onChange={e => ctx.updateCompanyOffice(o.id, { addressLine2: e.target.value })} /></Field>
                <Field label="Contact"><TextInput value={o.contactName} onChange={e => ctx.updateCompanyOffice(o.id, { contactName: e.target.value })} /></Field>
                <Field label="Phone"><TextInput value={o.phone} onChange={e => ctx.updateCompanyOffice(o.id, { phone: e.target.value })} /></Field>
                <Field label="Email"><TextInput value={o.email} onChange={e => ctx.updateCompanyOffice(o.id, { email: e.target.value })} /></Field>
              </div>
              <div className="flex justify-end mt-1">
                <button onClick={() => { if (confirm(`Remove ${o.name || 'this location'}?`)) ctx.removeCompanyOffice(o.id); }}
                  className="text-xs font-semibold text-[var(--leon-red)] hover:underline">Remove</button>
              </div>
            </div>
          ))}
        </div>
        </EditLock>
      )}
    </Collapsible>
  );
}

// One column of the profile's fact strip.
function CompanyBlock({ title, lines, contact, facts, sub, empty }) {
  return (
    <div className="px-5 py-4">
      <p className="text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45 mb-1.5">{title}</p>
      {sub && <p className="text-sm font-semibold">{sub}</p>}
      {lines && lines.map((l, i) => <p key={i} className="text-sm text-[var(--leon-black)]/75">{l}</p>)}
      {contact && contact.map((c, i) => <p key={`c${i}`} className="text-sm text-[var(--leon-black)]/55 mt-0.5">{c}</p>)}
      {facts && facts.map(f => (
        <p key={f.label} className="text-sm">
          <span className="text-[var(--leon-black)]/45">{f.label}</span> &middot; <b>{f.value}</b>
        </p>
      ))}
      {!lines && !contact && !(facts && facts.length) && (
        <p className="text-sm text-[var(--leon-black)]/35 italic">{empty || 'Not on file.'}</p>
      )}
    </div>
  );
}

// LEON Softwares — the specialty tools, built into the Hub. Each opens as a
// workspace here rather than as a link out, so a door here is the same door
// Production builds and Procurement buys.
//
// Every software wears the same chrome: the LEON mark, its own name, and its
// own colour. That is not decoration — six tools that each looked like their
// own app would read as six systems, and the whole point is that they are one.
const SOFTWARE_ACCENTS = {
  takeoff:     { tint: '#EEF2FF', edge: '#6366F1' },
  doors:       { tint: '#FFFBEB', edge: '#B08968' },
  stone:       { tint: '#F0FDFA', edge: '#14B8A6' },
  surfaces:    { tint: '#FDF2F8', edge: '#EC4899' },
  fenestration:{ tint: '#F7FEE7', edge: '#84CC16' },
  casework:    { tint: '#FFF7ED', edge: '#F59E0B' },
  sign:        { tint: '#EFF6FF', edge: '#2563EB' },
  studio:      { tint: '#FAF5FF', edge: '#A855F7' },
  countertops: { tint: '#F5F3FF', edge: '#7C3AED' },
  // LEON Office. One accent per application so the four are told apart at a
  // glance in LEON Studio, and a shared one for "all documents".
  office:            { tint: '#F1F5F9', edge: '#64748B' },
  office_word:       { tint: '#EFF6FF', edge: '#3B82F6' },
  office_sheet:      { tint: '#ECFDF5', edge: '#10B981' },
  office_slides:     { tint: '#FFF7ED', edge: '#F97316' },
  office_pdf:        { tint: '#FEF2F2', edge: '#DC2626' },
};

// The settings panel is mounted HERE, once, rather than added to each of the
// nine modules. Every software wears this chrome already, and its dials are
// declared as data in SOFTWARE_SETTINGS — so a tool gets its settings screen by
// existing, and adding a dial is one entry in data.jsx rather than an edit to a
// module file.
// A tool in its own browser tab. The Hub is one page with no router, so the
// link carries ?software=<key> and the app boots straight into that tool.
//
// Both tabs are the SAME app against the SAME localStorage: there is no server
// and no cross-tab sync, so two tabs editing one record is last-write-wins. The
// button says so rather than letting someone find out.
// A quote analysis in its own window: same page, told which job and which draft.
function openQuoteInNewTab(projectId, qaId) {
  const url = window.location.pathname + '?quote=' + encodeURIComponent(projectId + '~' + qaId);
  window.open(url, '_blank', 'noopener');
}
function openSoftwareInNewTab(key) {
  const url = window.location.pathname + '?software=' + encodeURIComponent(key);
  window.open(url, '_blank', 'noopener');
}
function OpenInTabButton({ softwareKey, className }) {
  return (
    <IconAction icon="⧉" className={className}
      title="Open this tool in its own browser tab — both tabs share the same data, and the last save wins"
      onClick={e => { if (e && e.stopPropagation) e.stopPropagation(); openSoftwareInNewTab(softwareKey); }} />
  );
}
function SoftwareChrome({ sw, onBack, children, ctx }) {
  const a = SOFTWARE_ACCENTS[sw.key] || { tint: 'var(--leon-cream)', edge: 'var(--leon-brown)' };
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [full, setFull] = useState(false);
  const schema = SOFTWARE_SETTINGS[sw.key];
  // FULL SCREEN for the whole tool. The rail has its own, which hides the side
  // panels; this one hides everything the app puts round a drawing — the header,
  // the nav, the masthead — because a shop drawing on an A2 sheet wants the
  // screen and a 1232px content column is not it. Escape comes back.
  useEffect(() => {
    if (!full) return undefined;
    const onKey = e => { if (e.key === 'Escape') setFull(false); };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
  }, [full]);
  if (full) {
    return (
      <div className="fixed inset-0 z-40 bg-white flex flex-col">
        <div className="no-print shrink-0 flex items-center gap-3 px-3 h-9 border-b border-[var(--leon-line)]"
          style={{ background: 'var(--leon-black)' }}>
          <img src="logo/leon-official-white.svg" alt="" className="h-5 w-auto" />
          <span className="text-white text-sm font-bold">{sw.name}</span>
          <button onClick={() => setFull(false)}
            className="ml-auto text-[11px] font-semibold text-white/70 hover:text-white">
            ⤡ Exit full screen <span className="text-white/35">(Esc)</span>
          </button>
        </div>
        <div className="flex-1 overflow-auto p-3">{children}</div>
      </div>
    );
  }
  return (
    <div>
      <div className="flex items-center gap-2 mb-3">
        <button onClick={onBack} className="no-print text-sm font-semibold text-[var(--leon-brown)]">
          ← LEON Studio
        </button>
        <div className="flex-1" />
        <button onClick={() => setFull(true)} title="Take this tool full screen (Esc to come back)"
          className="no-print text-[11px] font-semibold text-[var(--leon-black)]/45 hover:text-[var(--leon-brown)] mr-2">
          ⤢ Full screen
        </button>
        <span className="no-print"><OpenInTabButton softwareKey={sw.key} /></span>
      </div>
      <div className="rounded-xl overflow-hidden border border-[var(--leon-line)] mb-4">
        <div className="flex items-center gap-4 px-5 py-4" style={{ background: 'var(--leon-black)' }}>
          <img src="logo/leon-official-white.svg" alt="" className="h-11 w-auto shrink-0" />
          <span className="min-w-0">
            <span className="block text-white text-lg font-bold leading-tight tracking-wide">{sw.name}</span>
            <span className="block text-[11px] uppercase tracking-[0.2em]" style={{ color: a.edge }}>
              LEON Softwares
            </span>
          </span>
          <span className="ml-auto hidden md:block text-[11px] text-white/45 max-w-sm text-right leading-snug">
            {sw.blurb}
          </span>
          {schema && (
            <button onClick={() => setSettingsOpen(v => !v)} title={'Settings for ' + sw.name}
              className={`no-print shrink-0 ml-3 md:ml-0 rounded-lg px-2.5 py-1.5 text-sm border transition ${settingsOpen ? 'bg-white/15 border-white/40 text-white' : 'border-white/20 text-white/70 hover:text-white hover:border-white/40'}`}>
              <span aria-hidden="true">⚙️</span>
              <span className="ml-1.5 text-[11px] font-semibold uppercase tracking-wider">Settings</span>
            </button>
          )}
        </div>
        <div style={{ height: 3, background: a.edge }} />
      </div>
      {settingsOpen && schema && (
        <SoftwareSettingsPanel ctx={ctx} swKey={sw.key} schema={schema} accent={a}
          onClose={() => setSettingsOpen(false)} />
      )}
      <HubTools title={sw.name} heading={sw.name} />
      <div className="rounded-xl p-4" style={{ background: a.tint }}>{children}</div>
    </div>
  );
}

// One panel renders every software's settings, because they are declared as
// data. A saved value is only stored when it DIFFERS from the shipped default —
// the map stays sparse, and a default that changes later still reaches anyone
// who never overrode it.
function SoftwareSettingsPanel({ ctx, swKey, schema, accent, onClose }) {
  const all = ctx.softwareSettings || {};
  const saved = all[swKey] || {};
  const editable = !!(ctx.canEdit && ctx.canEdit('softwares'));

  function write(fieldKey, value) {
    if (!editable) return;
    ctx.setSoftwareSettings(prev => {
      const next = { ...(prev || {}) };
      const mine = { ...(next[swKey] || {}) };
      const def = softwareSettingDef(swKey, fieldKey);
      // Storing a value equal to the default would freeze it: a later change to
      // the shipped default could never reach this company again.
      if (value === null || value === '' || JSON.stringify(value) === JSON.stringify(def)) delete mine[fieldKey];
      else mine[fieldKey] = value;
      if (Object.keys(mine).length) next[swKey] = mine; else delete next[swKey];
      return next;
    });
  }
  function resetAll() {
    ctx.setSoftwareSettings(prev => { const n = { ...(prev || {}) }; delete n[swKey]; return n; });
  }

  const changed = Object.keys(saved).length;

  return (
    <div className="rounded-xl border border-[var(--leon-line)] bg-white mb-4 overflow-hidden" data-print-region>
      <div className="flex items-center gap-3 px-4 py-3 border-b border-[var(--leon-line)]" style={{ background: accent.tint }}>
        <span className="text-lg" aria-hidden="true">⚙️</span>
        <div className="min-w-0">
          <div className="font-bold text-sm">{schema.label} — settings</div>
          <div className="text-[11px] text-[var(--leon-black)]/55">
            The dials this tool works to. Everything here starts at the value the software ships with;
            anything you change is remembered as an override, so a default we improve later still reaches you.
          </div>
        </div>
        <div className="ml-auto flex items-center gap-2 shrink-0">
          {changed > 0 && <Badge tone="amber">{changed} changed</Badge>}
          {changed > 0 && editable && <Button size="sm" variant="outline" onClick={resetAll}>Reset to defaults</Button>}
          <Button size="sm" variant="outline" onClick={onClose}>Close</Button>
        </div>
      </div>
      {!editable && (
        <div className="px-4 py-2 text-[12px] text-[var(--leon-black)]/60 border-b border-[var(--leon-line)]">
          You can read these but not change them — editing needs edit rights on LEON Softwares.
        </div>
      )}
      <div className="p-4 grid md:grid-cols-2 gap-x-6 gap-y-5">
        {schema.sections.map(sec => (
          <div key={sec.name}>
            <div className="lp-section-title text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/60 mb-2">{sec.name}</div>
            <div className="space-y-3">
              {sec.fields.map(f => (
                <SoftwareSettingField key={f.key} field={f} editable={editable}
                  value={Object.prototype.hasOwnProperty.call(saved, f.key) ? saved[f.key] : f.def}
                  isOverride={Object.prototype.hasOwnProperty.call(saved, f.key)}
                  onChange={v => write(f.key, v)} />
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function SoftwareSettingField({ field, value, isOverride, editable, onChange }) {
  const f = field;
  const label = (
    <div className="flex items-baseline gap-2">
      <span className="text-[12px] font-semibold">{f.label}</span>
      {f.unit && <span className="text-[11px] text-[var(--leon-black)]/45">{f.unit}</span>}
      {isOverride && <span className="text-[10px] font-bold uppercase tracking-wider text-[var(--leon-brown)]">set here</span>}
    </div>
  );
  const hint = f.hint ? <div className="text-[11px] text-[var(--leon-black)]/50 mt-1">{f.hint}</div> : null;

  if (f.type === 'toggle') {
    return (
      <label className="flex items-start gap-2 cursor-pointer">
        <input type="checkbox" className="mt-1" checked={!!value} disabled={!editable}
          onChange={e => onChange(e.target.checked)} />
        <span className="min-w-0">{label}{hint}</span>
      </label>
    );
  }
  if (f.type === 'select') {
    return (
      <div>{label}
        <Select className="!w-full mt-1" value={value == null ? '' : value} disabled={!editable}
          onChange={e => onChange(e.target.value)}>
          {(f.options || []).map(o => <option key={o} value={o}>{o}</option>)}
        </Select>
        {hint}
      </div>
    );
  }
  if (f.type === 'list') {
    const list = Array.isArray(value) ? value : [];
    return (
      <div>{label}
        <TextArea className="!w-full mt-1 text-[12px]" rows={Math.min(8, Math.max(3, list.length))}
          value={list.join('\n')} disabled={!editable}
          onChange={e => onChange(e.target.value.split('\n').map(x => x.trim()).filter(Boolean))} />
        <div className="text-[11px] text-[var(--leon-black)]/50 mt-1">One per line.</div>
        {hint}
      </div>
    );
  }
  return (
    <div>{label}
      <TextInput className="!w-full mt-1" type={f.type === 'number' ? 'number' : 'text'}
        value={value == null ? '' : value} disabled={!editable}
        onChange={e => onChange(f.type === 'number'
          ? (e.target.value === '' ? null : Number(e.target.value))
          : e.target.value)} />
      {hint}
    </div>
  );
}

// ---------------------------------------------------------------------------
// LEON Studio — one place for everything you MAKE something in
// ---------------------------------------------------------------------------
// The specialty softwares and LEON Office were in two different places: Office
// was a top-level nav item and the softwares were buried in the Collection menu
// beside the catalogs. They are the same kind of thing — the tools you open to
// produce a drawing, a schedule, a quote, a deck or an issued PDF — and the
// catalogs are not. So they are gathered here, and Collection goes back to
// meaning the library.
//
// This is a router with a landing page, not a new hub with its own data: every
// tool below is the SAME component it always was, reading the same records.
function LeonStudioView({ ctx }) {
  // Which tool is open, held in App so the nav menu can open one directly.
  // '' is the tile grid — a real destination, listed as "All tools".
  const [open, setOpen] = useHubSection(ctx, 'leonStudio', '');
  // '' = all documents, or an OFFICE_APPS key. A deep link may open LEON Office
  // directly on one app — `goSoftware('office-slides', …)` sets it — and
  // OfficeHome then reads the `doc` param and opens that document. Both are
  // consumed once on mount, so a later render cannot drag you back here.
  const [officeApp, setOfficeApp] = useState(() =>
    (typeof swBootParam === 'function' ? swBootParam('office') : null) || null);

  const software = LEON_SOFTWARE_LINKS.find(s => s.key && s.key === open);
  if (software) {
    const Mod = studioModuleFor(software.key);
    return (
      <SoftwareChrome sw={software} ctx={ctx} onBack={() => setOpen(null)}>
        {Mod ? <Mod ctx={ctx} /> : <SoftwareMissing name={software.name} />}
      </SoftwareChrome>
    );
  }
  if (officeApp !== null) {
    const app = OFFICE_APPS.find(a => a.key === officeApp);
    const sw = { name: app ? app.label : 'LEON Office', icon: app ? app.icon : '🗂️',
                 blurb: app ? app.blurb : 'Word, Sheets, Presentation and PDF over one document record.' };
    return (
      <SoftwareChrome sw={sw} ctx={ctx} onBack={() => setOfficeApp(null)}>
        {typeof OfficeHome === 'function'
          ? <OfficeHome ctx={ctx} initialApp={officeApp || ''} />
          : <SoftwareMissing name="LEON Office" />}
      </SoftwareChrome>
    );
  }

  return (
    <div>
      <div className="rounded-xl overflow-hidden border border-[var(--leon-line)] mb-5">
        <div className="flex items-center gap-4 px-5 py-5" style={{ background: 'var(--leon-black)' }}>
          <img src="logo/leon-official-white.svg" alt="" className="h-14 w-auto shrink-0" />
          <span>
            <span className="block text-white text-2xl font-bold leading-tight">LEON Studio</span>
            <span className="block text-[11px] uppercase tracking-[0.22em] text-[var(--leon-brown-light,#b08968)]">
              Where the work gets made
            </span>
          </span>
        </div>
      </div>
      <p className="text-sm text-[var(--leon-black)]/55 mb-6 max-w-3xl">
        Every tool you open to <b>produce</b> something &mdash; a door schedule, a slab layout, a take-off,
        a cabinet, a render, a proposal, an issued PDF. They all read the same projects, scopes, vendors
        and catalogs as the rest of the Hub, so what you make here is the record itself, not a copy of it.
      </p>

      <NoticeCard notice={STUDIO_NOTICE} className="mb-6 max-w-3xl" />

      <ScratchWorkPanel ctx={ctx} />

      <StudioSection
        title="Design &amp; production"
        sub="Specialty tools that model real records — every one of them lets you enter the work by hand as well as import it."
        tiles={LEON_SOFTWARE_LINKS.map(sw => ({
          key: sw.key, name: sw.name, icon: sw.icon, blurb: sw.blurb,
          live: !!sw.key && !!studioModuleFor(sw.key),
          accent: SOFTWARE_ACCENTS[sw.key],
          newTab: true,
          onOpen: () => setOpen(sw.key),
        }))} />

      <StudioSection
        title="LEON Office"
        sub="Four applications over one document record — so a file knows which job, scope and vendor it belongs to."
        tiles={OFFICE_APPS.map(a => ({
          key: a.key, name: a.label, icon: a.icon, blurb: a.blurb,
          live: typeof OfficeHome === 'function',
          accent: SOFTWARE_ACCENTS['office_' + a.key] || SOFTWARE_ACCENTS.office,
          count: (ctx.officeDocs || []).filter(d => d.app === a.key && !d.archived).length,
          onOpen: () => setOfficeApp(a.key),
        })).concat([{
          key: 'all', name: 'All documents', icon: '🗂️',
          blurb: 'Every Office file: recent, favourites, templates, project files and the archive.',
          live: typeof OfficeHome === 'function',
          accent: SOFTWARE_ACCENTS.office,
          count: (ctx.officeDocs || []).filter(d => !d.archived).length,
          onOpen: () => setOfficeApp(''),
        }])} />
    </div>
  );
}

// One place that answers "is this software actually loaded", used both to route
// and to decide whether its tile is openable — so a tile can never offer a tool
// whose file is missing.
// Work done before there was a job to attach it to, and the way to attach it.
// It lives on the LEON Studio landing page because that is where every tool
// that could have produced it starts — one place to find it, rather than the
// same panel repeated in eight softwares.
function ScratchWorkPanel({ ctx }) {
  const sp = (ctx.scratchProjects || []).find(p => p.ownerId === ctx.currentUserId);
  const items = sp && typeof ctx.scratchContents === 'function' ? ctx.scratchContents(sp) : [];
  const [target, setTarget] = useState('');
  const [picked, setPicked] = useState([]);
  const [msg, setMsg] = useState('');
  if (!sp || !items.length) return null;
  const projects = ctx.deptProjects(ctx.projects || []);
  const total = items.reduce((n, i) => n + i.count, 0);

  function move() {
    const keys = picked.length ? picked : items.map(i => i.key);
    const n = ctx.moveScratchWork(sp.id, target, keys);
    const job = projects.find(p => p.id === target);
    setMsg(n ? `Moved ${n} record${n === 1 ? '' : 's'} to ${job ? job.name : 'the job'}.` : 'Nothing was moved.');
    setPicked([]); setTarget('');
  }

  return (
    <div className="mb-7 border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
      <div className="flex items-center gap-2 px-4 py-2.5 bg-[var(--leon-cream)] border-b border-[var(--leon-line)]">
        <span aria-hidden="true">📥</span>
        <span className="text-[12px] font-bold uppercase tracking-wider text-[var(--leon-black)]/65">Your unassigned work</span>
        <Badge tone="amber">{total} record{total === 1 ? '' : 's'}</Badge>
      </div>
      <div className="p-4">
        <p className="text-[12px] text-[var(--leon-black)]/55 mb-3 max-w-2xl">
          Work you started before there was a job to file it against. It is not counted as a job
          anywhere — no dashboard, report or pipeline sees it — and it stays here until you move it.
        </p>
        <div className="flex flex-wrap gap-2 mb-3">
          {items.map(i => {
            const on = picked.indexOf(i.key) >= 0;
            return (
              <button key={i.key}
                onClick={() => setPicked(p => on ? p.filter(k => k !== i.key) : p.concat([i.key]))}
                className={`text-[12px] px-2.5 py-1 rounded-lg border ${on
                  ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] font-semibold'
                  : 'border-[var(--leon-line)] text-[var(--leon-black)]/70'}`}>
                {i.label} <span className="opacity-55">{i.count}</span>
              </button>
            );
          })}
        </div>
        <div className="flex flex-wrap items-end gap-2">
          <Field label="Move to">
            <Select className="!w-72" value={target} onChange={e => setTarget(e.target.value)}>
              <option value="">— pick a job —</option>
              {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
          <Button disabled={!target} onClick={move}>
            {picked.length ? `Move ${picked.length} selected` : 'Move everything'}
          </Button>
          <span className="text-[11px] text-[var(--leon-black)]/45 max-w-sm">
            A move, not a copy — nothing is left behind, and the receiving job&rsquo;s change log records it.
          </span>
        </div>
        {msg && <p className="text-[12px] text-[var(--leon-brown)] font-semibold mt-2">{msg}</p>}
      </div>
    </div>
  );
}

function studioModuleFor(key) {
  return {
    doors: typeof DoorSoftware === 'function' ? DoorSoftware : null,
    stone: typeof StoneSoftware === 'function' ? StoneSoftware : null,
    surfaces: typeof SurfaceSoftware === 'function' ? SurfaceSoftware : null,
    takeoff: typeof DrawingSoftware === 'function' ? DrawingSoftware : null,
    fenestration: typeof FenestrationSoftware === 'function' ? FenestrationSoftware : null,
    casework: typeof CaseworkSoftware === 'function' ? CaseworkSoftware : null,
    sign: typeof SignSoftware === 'function' ? SignSoftware : null,
    studio: typeof StudioSoftware === 'function' ? StudioSoftware : null,
    logistics: typeof LogisticsSoftware === 'function' ? LogisticsSoftware : null,
    cad: typeof LeonCadSoftware === 'function' ? LeonCadSoftware : null,
    countertops: typeof CountertopSoftware === 'function' ? CountertopSoftware : null,
  }[key] || null;
}

function StudioSection({ title, sub, tiles }) {
  return (
    <div className="mb-7">
      <div className="lp-section-title text-sm font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/70 mb-1">{title}</div>
      <p className="text-xs text-[var(--leon-black)]/50 mb-3 max-w-3xl">{sub}</p>
      <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
        {tiles.map(t => {
          const a = t.accent || { tint: 'var(--leon-cream)', edge: 'var(--leon-line)' };
          return (
            // The "own tab" control is a SIBLING of the tile, positioned over
            // it — a button cannot be nested inside a button, and the tile
            // itself is one.
            <div key={t.key || t.name} className="relative">
            {t.live && t.newTab && (
              <span className="absolute top-2 right-2 z-10 no-print"><OpenInTabButton softwareKey={t.key} /></span>
            )}
            <button disabled={!t.live} onClick={() => t.live && t.onOpen()}
              className={`hub-card text-left bg-white rounded-xl overflow-hidden border transition w-full ${t.live ? 'border-[var(--leon-line)] hover:-translate-y-0.5 hover:shadow-md cursor-pointer' : 'border-[var(--leon-line)] opacity-55'}`}>
              <div style={{ height: 4, background: t.live ? a.edge : 'var(--leon-line)' }} />
              <div className="p-4">
                <div className="flex items-start gap-2.5 mb-1.5">
                  <span className="text-2xl leading-none rounded-lg p-1.5" aria-hidden="true"
                    style={{ background: t.live ? a.tint : 'var(--leon-cream)' }}>{t.icon}</span>
                  <span className="min-w-0 flex-1">
                    <span className="block text-sm font-bold leading-tight">{t.name}</span>
                    {/* "Trial version", not "Open" — these tools are in use but not yet
                        settled, and the team should expect to find things to fix.
                        Saying so on the tile is more honest than a green Open
                        badge that implies finished. */}
                    <Badge tone={t.live ? 'yellow' : 'neutral'}>{t.live ? 'Trial version' : 'On the roadmap'}</Badge>
                  </span>
                  {t.count !== undefined && (
                    <span className="text-lg font-bold text-[var(--leon-black)]/35 tabular-nums">{t.count}</span>
                  )}
                </div>
                <p className="text-xs text-[var(--leon-black)]/55">{t.blurb}</p>
              </div>
            </button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function LeonSoftwaresView({ ctx }) {
  const [open, setOpen] = useState(null);
  const active = LEON_SOFTWARE_LINKS.find(s => s.key && s.key === open);

  if (active) {
    const Mod = {
      doors: typeof DoorSoftware === 'function' ? DoorSoftware : null,
      stone: typeof StoneSoftware === 'function' ? StoneSoftware : null,
      surfaces: typeof SurfaceSoftware === 'function' ? SurfaceSoftware : null,
      takeoff: typeof DrawingSoftware === 'function' ? DrawingSoftware : null,
      fenestration: typeof FenestrationSoftware === 'function' ? FenestrationSoftware : null,
      casework: typeof CaseworkSoftware === 'function' ? CaseworkSoftware : null,
      sign: typeof SignSoftware === 'function' ? SignSoftware : null,
      studio: typeof StudioSoftware === 'function' ? StudioSoftware : null,
      logistics: typeof LogisticsSoftware === 'function' ? LogisticsSoftware : null,
      cad: typeof LeonCadSoftware === 'function' ? LeonCadSoftware : null,
      }[active.key];
    return (
      <SoftwareChrome sw={active} ctx={ctx} onBack={() => setOpen(null)}>
        {Mod ? <Mod ctx={ctx} /> : <SoftwareMissing name={active.name} />}
      </SoftwareChrome>
    );
  }

  return (
    <div>
      <div className="rounded-xl overflow-hidden border border-[var(--leon-line)] mb-5">
        <div className="flex items-center gap-4 px-5 py-5" style={{ background: 'var(--leon-black)' }}>
          <img src="logo/leon-official-white.svg" alt="" className="h-14 w-auto shrink-0" />
          <span>
            <span className="block text-white text-2xl font-bold leading-tight">LEON Softwares</span>
            <span className="block text-[11px] uppercase tracking-[0.22em] text-[var(--leon-brown-light,#b08968)]">
              Built into the Hub
            </span>
          </span>
        </div>
      </div>
      <p className="text-sm text-[var(--leon-black)]/55 mb-5 max-w-3xl">
        LEON&rsquo;s own specialty tools, built into the Hub rather than bolted onto it. They read the same
        projects, scopes, vendors and catalogs as everything else &mdash; so a door, a slab or a take-off
        here is the same record the rest of the business already works from.
      </p>
      <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
        {LEON_SOFTWARE_LINKS.map(sw => {
          const live = !!sw.key;
          const a = SOFTWARE_ACCENTS[sw.key] || { tint: 'var(--leon-cream)', edge: 'var(--leon-line)' };
          return (
            <button key={sw.name} disabled={!live} onClick={() => live && setOpen(sw.key)}
              className={`hub-card text-left bg-white rounded-xl overflow-hidden border transition ${live ? 'border-[var(--leon-line)] hover:-translate-y-0.5 hover:shadow-md cursor-pointer' : 'border-[var(--leon-line)] opacity-55'}`}>
              <div style={{ height: 4, background: live ? a.edge : 'var(--leon-line)' }} />
              <div className="p-4">
                <div className="flex items-start gap-2.5 mb-1.5">
                  <span className="text-2xl leading-none rounded-lg p-1.5" aria-hidden="true"
                    style={{ background: live ? a.tint : 'var(--leon-cream)' }}>{sw.icon}</span>
                  <span className="min-w-0">
                    <span className="block text-sm font-bold leading-tight">{sw.name}</span>
                    <Badge tone={live ? 'yellow' : 'neutral'}>{live ? 'Trial version' : 'On the roadmap'}</Badge>
                  </span>
                </div>
                <p className="text-xs text-[var(--leon-black)]/55">{sw.blurb}</p>
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// A software whose file has not loaded. Says which one and why rather than
// rendering a blank panel.
function SoftwareMissing({ name }) {
  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="font-semibold mb-1">{name} is not loaded</div>
      <div className="text-sm text-[var(--leon-black)]/55 max-w-md mx-auto">
        Its module file is missing from <code>softwares/</code> or is not listed in index.html.
      </div>
    </div>
  );
}

// Chart of Organization — a simple indented reporting tree over
// teamDirectory.reportsToId, so employees (and, per the request,
// subcontractors — though subcontractors are external companies without a
// natural internal manager, so v1 covers internal team members; a
// subcontractor's real "who to ask" relationship is their project's
// Logistic Manager/Project Coordinator, already visible on the project
// itself) can see who reports to whom.
// Classic pyramid layout — each person centered above their own direct
// reports, which fan out horizontally beneath them (so the chart naturally
// widens toward the bottom), connected by simple vertical drop-lines rather
// than measured/absolute-positioned SVG connectors — no charting library in
// this app, and this reads as a proper hierarchy without one.
// Depth palette. Tinted surfaces with a strong accent edge and dark text,
// rather than saturated blocks — the chart reads as light and open, and a
// person's name stays the most legible thing on their own card.
const ORG_LEVEL_COLORS = [
  { accent: '#F59E0B', tint: '#FFFBEB', line: '#FCD34D' },   // amber
  { accent: '#EC4899', tint: '#FDF2F8', line: '#F9A8D4' },   // pink
  { accent: '#6366F1', tint: '#EEF2FF', line: '#A5B4FC' },   // indigo
  { accent: '#14B8A6', tint: '#F0FDFA', line: '#5EEAD4' },   // teal
  { accent: '#84CC16', tint: '#F7FEE7', line: '#BEF264' },   // lime
];
function orgLevelColor(depth) { return ORG_LEVEL_COLORS[Math.min(depth, ORG_LEVEL_COLORS.length - 1)]; }
// What a person is called, not what the system calls them. A security role is
// an access level, not a job title, and it does not belong on a chart people
// read to find out who does what.
function personTitle(p) { return (p && p.title && p.title.trim()) || ''; }

function OrgChartNode({ ctx, person, byManager, depth, onOpen }) {
  const reports = byManager[person.id] || [];
  const c = orgLevelColor(depth || 0);
  const isMe = person.id === ctx.currentUserId;
  function countBelow(id) {
    return (byManager[id] || []).reduce((n, r) => n + 1 + countBelow(r.id), 0);
  }
  const below = countBelow(person.id);
  const title = personTitle(person);
  return (
    <div className="flex flex-col items-center">
      <button onClick={() => onOpen && onOpen(person.id)}
        className={`group relative flex items-center gap-2.5 rounded-lg pr-4 py-2 pl-2.5 whitespace-nowrap border shadow-sm transition hover:-translate-y-0.5 hover:shadow-md ${isMe ? 'ring-2 ring-offset-2' : ''}`}
        style={{ background: c.tint, borderColor: c.line, borderLeft: `4px solid ${c.accent}`, ringColor: c.accent }}
        title={`${person.name}${title ? ' — ' + title : ''}`}>
        <span className="rounded-full p-0.5 shrink-0" style={{ background: c.accent }}>
          <Avatar name={person.name} url={person.photoUrl} size={32} />
        </span>
        <span className="text-left">
          <span className="block text-sm font-bold leading-tight text-[var(--leon-black)]">
            {person.name}
            {isMe && <span className="ml-1.5 text-[9px] font-semibold uppercase tracking-wide" style={{ color: c.accent }}>you</span>}
          </span>
          {title
            ? <span className="block text-[11px] leading-tight text-[var(--leon-black)]/60">{title}</span>
            : <span className="block text-[11px] leading-tight text-[var(--leon-black)]/30 italic">title not set</span>}
          {person.officeLocation && (
            <span className="block text-[10px] leading-tight text-[var(--leon-black)]/45">{officeLabel(person.officeLocation)}</span>
          )}
          {below > 0 && (
            <span className="block text-[10px] leading-tight mt-0.5 font-semibold" style={{ color: c.accent }}>
              {reports.length} direct{below !== reports.length ? ` \u00b7 ${below} total` : ''}
            </span>
          )}
          {/* Dotted-line managers. The tree can only place this person once, so
              a second reporting line is stated rather than drawn. */}
          {(person.alsoReportsToIds || []).length > 0 && (
            <span className="block text-[10px] leading-tight mt-0.5 text-[var(--leon-black)]/45 italic">
              also reports to {person.alsoReportsToIds.map(id => personName(ctx.teamDirectory, id)).join(', ')}
            </span>
          )}
        </span>
      </button>
      {reports.length > 0 && (
        <>
          <div className="w-px h-5" style={{ background: c.line }} />
          <div className="flex items-start">
            {reports.map((r, i) => {
              const cc = orgLevelColor((depth || 0) + 1);
              const first = i === 0, last = i === reports.length - 1;
              return (
                <div key={r.id} className="flex flex-col items-center px-3">
                  <div className="relative w-full h-5">
                    {reports.length > 1 && (
                      <div className="absolute top-0 h-px" style={{ background: c.line, left: first ? '50%' : 0, right: last ? '50%' : 0 }} />
                    )}
                    <div className="absolute top-0 left-1/2 -translate-x-1/2 w-px h-5" style={{ background: cc.line }} />
                  </div>
                  <OrgChartNode ctx={ctx} person={r} byManager={byManager} depth={(depth || 0) + 1} onOpen={onOpen} />
                </div>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}
function OrgChartView({ ctx }) {
  const [openId, setOpenId] = useState(null);
  const [fullscreen, setFullscreen] = useState(false);
  const [fit, setFit] = useState(true);
  const [scale, setScale] = useState(1);
  // The tree's NATURAL size. A CSS transform paints smaller but does not change
  // layout size, so the wrapper has to be sized to width*scale by hand —
  // otherwise the box keeps its full-size footprint and the shrunken tree sits
  // somewhere inside a large empty area. scrollWidth/Height are unaffected by
  // the transform, which is why they can be measured while scaled.
  const [natural, setNatural] = useState({ w: 0, h: 0 });
  const frameRef = useRef(null);
  const treeRef = useRef(null);

  const active = ctx.teamDirectory.filter(p => p.active);
  const byManager = {};
  active.forEach(p => {
    if (p.reportsToId) (byManager[p.reportsToId] = byManager[p.reportsToId] || []).push(p);
  });
  Object.values(byManager).forEach(list => list.sort((a, b) => textAsc(a.name, b.name)));
  const roots = active.filter(p => !p.reportsToId || !active.some(m => m.id === p.reportsToId));
  const realRoots = roots.filter(p => (byManager[p.id] || []).length > 0);
  const unplaced = roots.filter(p => (byManager[p.id] || []).length === 0);

  // Scale the whole tree down until it fits the frame, so "the org chart" is
  // one glance rather than a scroll hunt. Never scales UP past 1 — a small
  // company should not get comically large cards.
  useEffect(() => {
    function measure() {
      const frame = frameRef.current, tree = treeRef.current;
      if (!frame || !tree) return;
      const w = tree.scrollWidth, h = tree.scrollHeight;
      if (!w || !h) return;
      setNatural({ w, h });
      if (!fit) { setScale(1); return; }
      const avail = frame.clientWidth - 24;
      const availH = (fullscreen ? window.innerHeight - 170 : 600);
      setScale(Math.min(1, avail / w, availH / h));
    }
    measure();
    const t = setTimeout(measure, 80);          // after fonts/images settle
    window.addEventListener('resize', measure);
    return () => { clearTimeout(t); window.removeEventListener('resize', measure); };
  }, [fit, fullscreen, ctx.teamDirectory, realRoots.length]);

  const chart = (
    <div ref={frameRef} className={`rounded-xl bg-gradient-to-b from-[var(--leon-cream)] to-transparent ${fullscreen ? 'flex-1 overflow-auto' : 'overflow-auto'}`}
         style={fullscreen ? {} : { maxHeight: 640 }}>
      <div className="mx-auto" style={natural.w ? { width: natural.w * scale, height: natural.h * scale } : {}}>
        <div className="p-6 w-max" ref={treeRef}
             style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}>
          <div className="flex items-start gap-14">
            {realRoots.map(r => <OrgChartNode key={r.id} ctx={ctx} person={r} byManager={byManager} depth={0} onOpen={setOpenId} />)}
          </div>
        </div>
      </div>
    </div>
  );

  const controls = (
    <div className="flex items-center gap-1.5 flex-wrap">
      <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
        {[{ k: true, l: 'Fit to screen' }, { k: false, l: 'Actual size' }].map(o => (
          <button key={String(o.k)} onClick={() => setFit(o.k)}
            className={`px-2.5 py-1 rounded-md text-xs font-semibold ${fit === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
        ))}
      </div>
      <Button size="sm" variant="outline" onClick={() => setFullscreen(f => !f)}>
        {fullscreen ? 'Exit full screen' : '\u26f6 Full screen'}
      </Button>
    </div>
  );

  const legend = (
    <div className="flex items-center gap-2 flex-wrap">
      {['Ownership', 'Directors', 'Managers', 'Leads', 'Team'].map((l, i) => (
        <span key={l} className="flex items-center gap-1 text-[10px] font-semibold text-[var(--leon-black)]/50">
          <span className="w-3 h-3 rounded border" style={{ background: orgLevelColor(i).tint, borderColor: orgLevelColor(i).accent, borderLeftWidth: 3 }} />
          {l}
        </span>
      ))}
    </div>
  );

  if (fullscreen) {
    return (
      <div className="fixed inset-0 z-50 bg-white flex flex-col p-4">
        <div className="flex items-center gap-3 flex-wrap mb-3">
          <h2 className="text-lg font-bold">Organization Chart</h2>
          {legend}
          <div className="flex-1" />
          {controls}
        </div>
        {chart}
        <MyProfileModal open={!!openId} onClose={() => setOpenId(null)} ctx={ctx} targetUserId={openId} />
      </div>
    );
  }

  return (
    <div>
      <div className="flex items-start justify-between gap-3 flex-wrap mb-3">
        <p className="text-sm text-[var(--leon-black)]/50 max-w-xl">
          Who reports to whom. Set each person&rsquo;s manager and job title under Users;
          colour marks the level, and clicking anyone opens their profile. Someone who
          answers to two people gets a primary manager, which places their card, plus
          a dotted line noted on it.
        </p>
        {legend}
      </div>
      <div className="flex items-center justify-end mb-2">{controls}</div>

      {realRoots.length === 0
        ? <EmptyState text="No reporting structure set up yet — set each person's manager under Users." />
        : chart}

      {unplaced.length > 0 && (
        <Collapsible title="Not yet placed on the chart" count={unplaced.length}>
          <p className="text-xs text-[var(--leon-black)]/50 mb-2">
            These people have no manager set and nobody reporting to them. Set a manager under
            Users and they join the tree.
          </p>
          <div className="flex flex-wrap gap-1.5">
            {unplaced.map(p => (
              <button key={p.id} onClick={() => setOpenId(p.id)}
                className="flex items-center gap-2 border border-[var(--leon-line)] rounded-lg px-2 py-1.5 hover:border-[var(--leon-brown-light)] transition">
                <Avatar name={p.name} url={p.photoUrl} size={24} />
                <span className="text-left">
                  <span className="block text-xs font-semibold leading-tight">{p.name}</span>
                  <span className={`block text-[10px] leading-tight ${personTitle(p) ? 'text-[var(--leon-black)]/45' : 'text-[var(--leon-black)]/25 italic'}`}>
                    {personTitle(p) || 'title not set'}
                    {p.officeLocation ? <span className="text-[var(--leon-black)]/40"> · {officeLabel(p.officeLocation)}</span> : null}
                  </span>
                </span>
              </button>
            ))}
          </div>
        </Collapsible>
      )}

      <MyProfileModal open={!!openId} onClose={() => setOpenId(null)} ctx={ctx} targetUserId={openId} />
    </div>
  );
}

// "Meet the Team" (Phase 10) — a grid of every active person's card;
// clicking opens their full profile via MyProfileModal, read-only for
// everyone else and editable inline for the owner/Admin (that gating lives
// in MyProfileModal itself). Chart of Organization lives here too, as a
// second internal subtab, rather than its own top-level nav item.
const MEET_THE_TEAM_SUBTABS = [
  { key: 'team', label: 'Team', icon: '👥' },
  // Subcontractors are not employees, but they are who the job is actually
  // built with — everyone should be able to see who is on the trade side
  // without needing access to the subcontractor RECORD, which carries rates,
  // insurance and invoices.
  { key: 'subs', label: 'Subcontractors', icon: '🔧' },
  { key: 'orgChart', label: 'Org Chart', icon: '🗂️' },
];
const TEAM_SORT_OPTIONS = [
  { key: 'name-asc', label: 'Name (A–Z)' },
  { key: 'name-desc', label: 'Name (Z–A)' },
  { key: 'role-asc', label: 'Role (A–Z)' },
];
const TEAM_SORT_COMPARATORS = {
  'name-asc': (a, b) => textAsc(a.name, b.name),
  'name-desc': (a, b) => textDesc(a.name, b.name),
  'role-asc': (a, b) => textAsc(a.securityRole, b.securityRole),
};
// A read-only directory of the trade partners, for everyone. Deliberately NOT
// the subcontractor record: no rates, no insurance documents, no invoices —
// those live in the Subcontractors screen behind its own permission. This is
// "who are we working with", which is a question the whole team has.
function MeetTheSubcontractors({ ctx }) {
  const [search, setSearch] = useState('');
  const [trade, setTrade] = useState('All');
  const [openId, setOpenId] = useState(null);
  const all = (ctx.subcontractors || []).filter(s => s.status === 'Active');
  const q = search.trim().toLowerCase();
  const rows = all.filter(s => {
    if (trade !== 'All' && s.trade !== trade) return false;
    if (!q) return true;
    return [s.companyName, s.contactName, s.trade, s.officeLocation, s.city, s.state]
      .some(v => (v || '').toLowerCase().includes(q));
  }).sort((a, b) => textAsc(a.companyName, b.companyName));
  const trades = Array.from(new Set(all.map(s => s.trade).filter(Boolean))).sort();
  const open = openId ? all.find(s => s.id === openId) : null;

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/55 mb-4 max-w-3xl">
        The trade partners we build with. Everyone can see who they are and how to reach them; rates,
        insurance and invoices stay on the subcontractor record, behind its own permission.
      </p>
      <div className="flex flex-wrap items-center gap-2 mb-4">
        <TextInput value={search} onChange={e => setSearch(e.target.value)}
          placeholder="Search by company, contact, trade, or office…" className="!w-72" />
        <Select value={trade} onChange={e => setTrade(e.target.value)} className="!w-auto">
          <option value="All">All Trades</option>
          {trades.map(t => <option key={t} value={t}>{t}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{rows.length} of {all.length}</span>
      </div>
      {!rows.length ? <EmptyState text="No active subcontractors yet." /> : (
        <div className="grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
          {rows.map(s => (
            <button key={s.id} onClick={() => setOpenId(s.id)}
              className="border border-[var(--leon-line)] rounded-lg p-3 bg-white hover:border-[var(--leon-brown)] hover:bg-[var(--leon-cream)] text-left transition-colors">
              <Avatar name={s.companyName || s.contactName} url={s.logoUrl || null} size={48} />
              <p className="text-sm font-bold mt-2 truncate">{s.companyName || s.contactName}</p>
              <p className="text-xs text-[var(--leon-black)]/50 truncate">{s.trade}</p>
              {s.contactName && <p className="text-[11px] text-[var(--leon-black)]/45 truncate">{s.contactName}</p>}
              {s.officeLocation && (
                <p className="text-[11px] text-[var(--leon-black)]/40 truncate">{officeLabel(s.officeLocation)}</p>
              )}
            </button>
          ))}
        </div>
      )}
      <Modal open={!!open} onClose={() => setOpenId(null)} title={open ? (open.companyName || open.contactName) : ''}>
        {open && (
          <div className="space-y-3">
            <div className="flex items-center gap-3">
              <Avatar name={open.companyName || open.contactName} url={open.logoUrl || null} size={56} />
              <div className="min-w-0">
                <div className="font-bold">{open.companyName || open.contactName}</div>
                <div className="text-sm text-[var(--leon-black)]/55">{open.trade}</div>
                {open.officeLocation && <div className="text-xs text-[var(--leon-black)]/45">{officeLabel(open.officeLocation)}</div>}
              </div>
            </div>
            <div className="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm">
              {open.contactName && <div><span className="text-[var(--leon-black)]/50">Contact</span><div>{open.contactName}</div></div>}
              {open.email && <div><span className="text-[var(--leon-black)]/50">Email</span><div><a className="text-[var(--leon-brown)] hover:underline" href={'mailto:' + open.email}>{open.email}</a></div></div>}
              {open.phone && <div><span className="text-[var(--leon-black)]/50">Phone</span><div>{open.phone}</div></div>}
              {open.mobile && <div><span className="text-[var(--leon-black)]/50">Mobile</span><div>{open.mobile}</div></div>}
              {(open.city || open.state) && <div><span className="text-[var(--leon-black)]/50">Based</span><div>{[open.city, open.state].filter(Boolean).join(', ')}</div></div>}
            </div>
            {open.notes && <p className="text-sm text-[var(--leon-black)]/60 whitespace-pre-wrap">{open.notes}</p>}
            <p className="text-[11px] text-[var(--leon-black)]/45">
              Rates, insurance, agreements and invoices live on the subcontractor record under
              Vendors, which follows its own permission.
            </p>
          </div>
        )}
      </Modal>
    </div>
  );
}

function MeetTheTeamView({ ctx, embedded }) {
  const [sub, setSub] = useState('team');
  const [openId, setOpenId] = useState(null);
  const [search, setSearch] = useState('');
  const [sortKey, setSortKey] = useState('name-asc');
  const active = ctx.teamDirectory.filter(p => p.active);
  const q = search.trim().toLowerCase();
  const filtered = active.filter(p => !q || [p.name, p.title, p.securityRole, p.officeLocation].some(v => (v || '').toLowerCase().includes(q)));
  const sorted = sortList(filtered, sortKey, TEAM_SORT_COMPARATORS);
  return (
    <div>
      {!embedded && <>
        <h1 className="text-2xl font-bold mb-1">Meet the Team</h1>
        <p className="text-sm text-[var(--leon-black)]/50 mb-5">Everyone active at LEON Integra.</p>
      </>}
      {!embedded && (
        <div className="flex gap-1 mb-5 border-b border-[var(--leon-line)]">
          {MEET_THE_TEAM_SUBTABS.map(t => (
            <button key={t.key} onClick={() => setSub(t.key)} className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 ${sub === t.key ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50 hover:text-[var(--leon-black)]'}`}>{t.icon && <span aria-hidden="true" className="mr-1.5 opacity-80">{t.icon}</span>}{t.label}</button>
          ))}
        </div>
      )}
      {sub === 'team' && (
        <>
          <div className="flex flex-wrap items-center gap-2 mb-4">
            <TextInput value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by name, title, or role…" className="!w-64" />
            <SortSelect value={sortKey} onChange={setSortKey} options={TEAM_SORT_OPTIONS} />
            <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{sorted.length} of {active.length}</span>
          </div>
          <div className="grid sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
          {sorted.map(p => (
            <button key={p.id} onClick={() => setOpenId(p.id)} className="border border-[var(--leon-line)] rounded-lg p-3 bg-white hover:border-[var(--leon-brown)] hover:bg-[var(--leon-cream)] text-left transition-colors">
              <Avatar name={p.name} url={p.photoUrl} size={48} />
              <p className="text-sm font-bold mt-2 truncate">{p.name}</p>
              <p className="text-xs text-[var(--leon-black)]/50 truncate">{p.title || p.securityRole}</p>
              {p.officeLocation && (
                <p className="text-[11px] text-[var(--leon-black)]/40 truncate">{officeLabel(p.officeLocation)}</p>
              )}
            </button>
          ))}
          </div>
        </>
      )}
      {sub === 'subs' && <MeetTheSubcontractors ctx={ctx} />}
      {sub === 'orgChart' && <OrgChartView ctx={ctx} />}
      <MyProfileModal open={!!openId} onClose={() => setOpenId(null)} ctx={ctx} targetUserId={openId} />
    </div>
  );
}
// Lets Admin load/reset the one-project demo scenario used for team
// walkthroughs (§ team walkthrough request) — a complete, realistic project
// touching every major module. Safe to click again before a rehearsal: it
// replaces the previous run rather than duplicating it.
function DemoScenarioPanel({ ctx }) {
  const existing = ctx.projects.find(p => p.projectNumber === 'LI-DEMO-001');
  function load() {
    const projectId = ctx.loadDemoScenario();
    ctx.goProject(projectId);
  }
  return (
    <Collapsible title="Demo Scenario" right={<div className="flex gap-2"><Button size="sm" onClick={load}>{existing ? 'Reset Demo Scenario' : '+ Load Demo Scenario'}</Button>{existing && <Button size="sm" variant="ghost" onClick={ctx.removeDemoScenario}>Remove</Button>}</div>}>
      <p className="text-xs text-[var(--leon-black)]/50">
        Adds one complete, realistic project — an account, vendor, subcontractor, contract and payment terms, a vendor Estimate → PO → PI chain,
        an Export Container that's arrived and been handed to Logistics (ready to receive live), a pending delivery and installation to approve,
        an approved change order, AP invoices, a shop drawing submittal, a task, a meeting, and both a Client Portal and Subcontractor Portal login —
        for demoing the app to the team without setting any of it up by hand. Loading again replaces the previous run rather than duplicating it.
      </p>
      {existing && <p className="text-xs text-[var(--leon-brown)] font-semibold mt-2">Currently loaded: {existing.name} ({existing.projectNumber}).</p>}
    </Collapsible>
  );
}
// ══════════════════════════════════════════════ Render Library
// LEON's own option boards, at two levels. ITEMS are the point: every door
// type, vanity, mirror, edge profile and kitchen as its own picture with its
// real name, so a selection, a quote line or a slide can point at one. BOARDS
// are the whole published sheets, kept because they are what gets printed large
// and handed to a client.
//
// Real files under renders/, never localStorage — the set is over 100 MB.
const RENDER_CATEGORIES = ['All', 'Doors', 'Kitchens', 'Vanities', 'Mirrors', 'Countertops', 'Baseboards'];

function renderItems() { return typeof RENDER_ITEMS === 'undefined' ? [] : RENDER_ITEMS; }
function renderBoards() { return typeof RENDER_BOARDS === 'undefined' ? [] : RENDER_BOARDS; }
function renderItemById(id) { return renderItems().find(i => i.id === id) || null; }
function fmtMB(b) { return `${((b || 0) / 1048576).toFixed(1)} MB`; }

function RenderLibraryTab({ ctx, editable }) {
  const [cat, setCat] = useState('All');
  const [q, setQ] = useState('');
  const [board, setBoard] = useState('');
  const [open, setOpen] = useState(null);
  const [openBoard, setOpenBoard] = useState(null);
  const [view, setView] = useState('items');

  const items = renderItems().filter(i => {
    if (cat !== 'All' && i.cat !== cat) return false;
    if (board && i.board !== board) return false;
    if (!q.trim()) return true;
    const hay = `${i.name} ${i.sub} ${i.group || ''} ${i.style} ${i.finish} ${i.brand} ${i.cat}`.toLowerCase();
    return hay.includes(q.trim().toLowerCase());
  });
  const boards = renderBoards().filter(b => cat === 'All' || b.cat === cat);

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h3 className="text-lg font-bold">Render Library</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            Our own options, one picture each — {renderItems().length} of them, cut from the
            {' '}{renderBoards().length} published boards and carried with their real names. Pick one for a
            selection, a quote or a slide; the whole board is still here when you need to print it large.
          </p>
        </div>
        <Field label="Search">
          <TextInput className="w-52" value={q} onChange={e => setQ(e.target.value)} placeholder="Pocket door, ogee, walnut…" />
        </Field>
      </div>

      <div className="flex items-center gap-2 flex-wrap">
        {RENDER_CATEGORIES.map(c => {
          const n = c === 'All' ? renderItems().length : renderItems().filter(i => i.cat === c).length;
          if (!n) return null;
          return (
            <button key={c} onClick={() => { setCat(c); setBoard(''); }}
              className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition ${cat === c ? 'bg-[var(--leon-brown)] text-white border-[var(--leon-brown)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]'}`}>
              {c} <span className="opacity-60">{n}</span>
            </button>
          );
        })}
        <div className="ml-auto flex items-center gap-2">
          <Select className="w-56" value={board} onChange={e => setBoard(e.target.value)}>
            <option value="">All boards</option>
            {boards.map(b => <option key={b.id} value={b.id}>{b.name}{b.finish ? ` — ${b.finish}` : ''} ({b.n})</option>)}
          </Select>
          <div className="flex rounded-lg border border-[var(--leon-line)] overflow-hidden">
            {[['items', 'Items'], ['boards', 'Boards']].map(([k, l]) => (
              <button key={k} onClick={() => setView(k)}
                className={`px-3 py-1.5 text-xs font-semibold ${view === k ? 'bg-[var(--leon-brown)] text-white' : 'hover:bg-[var(--leon-cream)]'}`}>{l}</button>
            ))}
          </div>
        </div>
      </div>

      {view === 'items' && (
        <>
          <div className="grid gap-3 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5">
            {items.map(it => (
              <button key={it.id} onClick={() => setOpen(it)}
                className="text-left rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden hover:border-[var(--leon-brown)] transition group">
                <div className="bg-[var(--leon-cream)] aspect-[4/3] overflow-hidden">
                  <img src={it.thumb} alt={it.name} loading="lazy"
                    className="w-full h-full object-cover group-hover:scale-[1.03] transition-transform" />
                </div>
                <div className="p-2">
                  <div className="text-[13px] font-bold leading-tight">{it.name}</div>
                  {it.sub && <div className="text-[11px] text-[var(--leon-black)]/50 leading-tight">{it.sub}</div>}
                  <div className="text-[10px] uppercase tracking-wide text-[var(--leon-brown)] mt-1">
                    {it.finish || it.style || it.cat}
                  </div>
                </div>
              </button>
            ))}
          </div>
          {!items.length && <EmptyState text="Nothing matches that search." />}
        </>
      )}

      {view === 'boards' && (
        <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
          {boards.map(b => (
            <div key={b.id} className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
              <button onClick={() => setOpenBoard(b)} className="block w-full bg-[var(--leon-cream)]">
                <img src={b.thumb || b.web || b.master} alt={b.name} loading="lazy" className="w-full h-auto" />
              </button>
              <div className="p-3">
                <div className="text-[10px] uppercase tracking-widest text-[var(--leon-brown)] font-semibold">{b.brand}</div>
                <div className="font-bold leading-tight">{b.name}</div>
                <div className="text-xs text-[var(--leon-black)]/55 mb-2">{b.finish || `${b.n} options`}</div>
                <div className="flex items-center gap-2 text-[11px]">
                  {b.print && <a href={b.print} download className="font-semibold text-[var(--leon-brown)]">⬇ Print file</a>}
                  <a href={b.master} download className="font-semibold text-[var(--leon-black)]/55">Original</a>
                  <button onClick={() => { setBoard(b.id); setView('items'); }}
                    className="ml-auto font-semibold text-[var(--leon-brown)]">{b.n} items →</button>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}

      <RenderItemModal item={open} onClose={() => setOpen(null)} ctx={ctx} />
      <RenderBoardModal board={openBoard} onClose={() => setOpenBoard(null)} ctx={ctx} />

      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-4 text-xs text-[var(--leon-black)]/65 space-y-2">
        <div className="font-semibold text-[var(--leon-black)] text-sm">About these files</div>
        <p>
          Each item is cut from its board at full resolution and enlarged twice with a light sharpen,
          which keeps it clean at slide and letter size. The boards additionally carry a 3&times; print
          file tagged at 300 dpi, so a whole sheet places at about <b>15 &times; 10 inches</b> rather than
          being treated as a 21-inch 72 dpi image.
        </p>
        <p>
          <b>Enlarging cannot add detail that was never in the file.</b> For anything bigger than roughly
          15 inches — a booth panel, a showroom board — export the board again from the design file it
          was made in, at the size it will actually be printed.
        </p>
      </div>
    </div>
  );
}

function RenderItemModal({ item, onClose, ctx }) {
  if (!item) return null;
  const board = renderBoards().find(b => b.id === item.board);
  const siblings = renderItems().filter(i => i.board === item.board);
  return (
    <Modal open={!!item} onClose={onClose} wide title={item.name}
      footer={
        <div className="flex items-center justify-between w-full gap-3 flex-wrap">
          <div className="text-xs text-[var(--leon-black)]/50">
            {item.w} &times; {item.h} px · item {item.n} of {siblings.length} on {board ? board.name : item.board}
          </div>
          <div className="flex gap-2">
            <ShareButton ctx={ctx} subject={`${item.name}${item.sub ? ` — ${item.sub}` : ''}`}
              summary={`LEON ${item.cat} option`} subjectKey={`renderItem:${item.id}`} label="Share" />
            <a href={item.img} download><Button>⬇ Download</Button></a>
          </div>
        </div>
      }>
      <div className="space-y-3">
        <img src={item.img} alt={item.name} className="w-full h-auto rounded border border-[var(--leon-line)] bg-[var(--leon-cream)]" />
        <div className="flex items-center gap-2 flex-wrap text-xs">
          <Badge tone="brown">{item.brand}</Badge>
          <Badge>{item.cat}</Badge>
          {item.group && <Badge>{item.group}</Badge>}
          {item.finish && <Badge>{item.finish}</Badge>}
          {item.sub && <span className="text-[var(--leon-black)]/60 italic">{item.sub}</span>}
        </div>
        {item.style && <div className="text-xs text-[var(--leon-black)]/50">From <b>{item.style}</b></div>}
      </div>
    </Modal>
  );
}

function RenderBoardModal({ board, onClose, ctx }) {
  if (!board) return null;
  return (
    <Modal open={!!board} onClose={onClose} wide title={board.name}
      footer={
        <div className="flex items-center justify-between w-full gap-3 flex-wrap">
          <div className="text-xs text-[var(--leon-black)]/50">
            Original {board.w} &times; {board.h} px · print {board.printIn}
          </div>
          <div className="flex gap-2">
            <ShareButton ctx={ctx} subject={board.name} summary={`LEON board · ${board.n} options`}
              subjectKey={`renderBoard:${board.id}`} label="Share" />
            <a href={board.master} download><Button variant="ghost">Original · {fmtMB(board.masterBytes)}</Button></a>
            {board.print && <a href={board.print} download><Button>⬇ Print file · {fmtMB(board.printBytes)}</Button></a>}
          </div>
        </div>
      }>
      <img src={board.web || board.master} alt={board.name}
        className="w-full h-auto rounded border border-[var(--leon-line)]" />
    </Modal>
  );
}


// ── Quote analysis defaults ─────────────────────────────────────────────────
// The company's own numbers, edited once here instead of retyped on every
// quotation — the same idea as the Lead Times tab, and asked for in the same
// words. Every figure was read out of the Revere Street workbook, where it was
// buried inside a formula; this screen is the first time any of them has been
// visible, let alone editable.
//
// A quotation COPIES these when a scope is created, exactly as a scope copies
// its stage template. Changing a default therefore never reprices a quote that
// has already been drafted — which is the behaviour anyone would want and the
// opposite of what a live link would do.
const QUOTE_RECIPE_FIELDS = [
  { key: 'uom', label: 'Unit', kind: 'text', hint: 'What this scope is measured in.' },
  { key: 'costDriver', label: 'Counts by', kind: 'driver',
    hint: 'What a container fraction is measured against.' },
  { key: 'pieceLengthIn', label: 'Stick length', kind: 'num', suffix: 'in',
    hint: 'Trim only — linear feet become sticks of this length.' },
  { key: 'containerCapacity', label: 'Per container', kind: 'num',
    hint: 'How many of the counted thing fill one container.' },
  { key: 'freightPerContainer', label: 'Ocean', kind: 'money' },
  { key: 'inlandPerContainer', label: 'Inland', kind: 'money' },
  { key: 'brokerPerContainer', label: 'Broker', kind: 'money' },
  { key: 'overheadPct', label: 'Overhead', kind: 'pct', hint: 'On material, before freight.' },
  { key: 'slabYield', label: 'Yield per slab', kind: 'num', suffix: 'sq ft',
    hint: 'Stone only — what one slab actually yields.' },
  { key: 'slabWastePct', label: 'Slab waste', kind: 'pct', hint: 'Stone only.' },
];


// ── What a quoted line of this trade must say ───────────────────────────────
// An ordered, editable list of specification fields per scope. Casework was
// written out by the client; every other trade ships as a draft read from the
// take-off's own questions plus the things that actually move a price, and is
// meant to be corrected here rather than in the source.
//
// The list is the WHOLE answer for a trade, not a set of additions: removing a
// field removes it, and `mergeNewQuoteSpecs` therefore keeps an edited list
// entire rather than appending the seed back onto it.
function QuoteSpecFieldsEditor({ ctx, scopeKey, editable, standalone }) {
  const [adding, setAdding] = useState('');
  const specs = ctx.quoteSpecs || {};
  const seed = quoteSpecSeed();
  const key = quoteScopeSpecKey(scopeKey);
  const base = seed[key] || [];
  // No entry in the map means this trade follows the standard — the map holds
  // only genuine overrides, so "changed" is simply "has an entry".
  const list = specs[key] || base;
  const changed = !!specs[key];

  // `next === null` means FOLLOW THE STANDARD: the key is deleted rather than
  // written back as a copy of the seed. Storing a list identical to the seed
  // freezes it — `mergeNewQuoteSpecs` keeps a persisted list entire, so a trade
  // "reset" by writing the seed could never receive a better standard again.
  // Same rule as softwareSettings: a value equal to the default is not stored.
  function write(next) {
    if (!editable) return;
    ctx.setQuoteSpecs(prev => {
      const out = Object.assign({}, prev);
      if (next === null) delete out[key]; else out[key] = next;
      return out;
    });
  }
  const move = (i, d) => {
    const next = list.slice();
    const j = i + d;
    if (j < 0 || j >= next.length) return;
    next[i] = list[j]; next[j] = list[i];
    write(next);
  };

  return (
    <div className={standalone ? '' : 'mt-3 pt-3 border-t border-[var(--leon-line)]'}>
      <div className="flex items-baseline gap-2 mb-2 flex-wrap">
        <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/50">
          What a line must say
        </span>
        <span className="text-[11px] text-[var(--leon-black)]/40">
          {list.length} field{list.length === 1 ? '' : 's'} on every {quoteScopeLabel(key)} line
        </span>
        {changed && !standalone && <Badge tone="yellow">changed from standard</Badge>}
      </div>
      {!list.length && (
        <p className="text-[11px] text-[var(--leon-black)]/45 mb-2">
          Nothing asked for on this trade&rsquo;s lines &mdash; they carry their description, quantity and
          price and no specification. {changed && 'Reset to standard brings the suggested fields back.'}
        </p>
      )}
      <div className="flex flex-wrap gap-1.5">
        {list.map((f, i) => (
          <span key={f + i}
            className="inline-flex items-center gap-1 rounded-full border border-[var(--leon-line)] bg-white pl-2.5 pr-1 py-0.5 text-xs">
            <span className="text-[var(--leon-black)]/30 tabular-nums text-[10px]">{i + 1}</span>
            {f}
            {editable && (
              <span className="flex items-center">
                <button onClick={() => move(i, -1)} disabled={i === 0} title="Move earlier"
                  className="px-0.5 text-[var(--leon-black)]/30 hover:text-[var(--leon-brown)] disabled:opacity-20">&#8593;</button>
                <button onClick={() => move(i, 1)} disabled={i === list.length - 1} title="Move later"
                  className="px-0.5 text-[var(--leon-black)]/30 hover:text-[var(--leon-brown)] disabled:opacity-20">&#8595;</button>
                <button onClick={() => write(list.filter((x, k2) => k2 !== i))} title={`Remove "${f}"`}
                  className="px-1 text-[var(--leon-black)]/25 hover:text-[var(--leon-red)]">&#10005;</button>
              </span>
            )}
          </span>
        ))}
      </div>
      {editable && (
        <div className="flex items-center gap-1.5 mt-2 flex-wrap">
          <TextInput className="!w-56 !py-1 !text-xs" value={adding} placeholder="Add a field — e.g. Edge Profile"
            onChange={e => setAdding(e.target.value)}
            onKeyDown={e => {
              if (e.key === 'Enter' && adding.trim()) { write(list.concat(adding.trim())); setAdding(''); }
            }} />
          <Button size="sm" variant="ghost" disabled={!adding.trim()}
            onClick={() => { write(list.concat(adding.trim())); setAdding(''); }}>Add</Button>
          <div className="flex-1" />
          {/* Starting from nothing is a legitimate answer, and it should not
              cost twenty presses of the same ✕. */}
          {!!list.length && (
            <button
              onClick={() => { if (confirm(`Remove all ${list.length} fields from a ${quoteScopeLabel(key)} line?`)) write([]); }}
              className="text-xs font-semibold text-[var(--leon-black)]/45 hover:text-[var(--leon-red)]">
              Clear all
            </button>
          )}
          {changed && (
            <button onClick={() => write(null)}
              className="text-xs font-semibold text-[var(--leon-brown)] ml-1">Reset to standard</button>
          )}
        </div>
      )}
      <p className="text-[10px] text-[var(--leon-black)]/40 mt-1.5">
        A quotation reads this list live, so a field added here appears on every line of this trade at
        once &mdash; including on quotations already drafted. Answers already typed are kept.
      </p>
    </div>
  );
}


// ── Quotation Lines ─────────────────────────────────────────────────────────
// What a quotation line ASKS FOR, trade by trade. This is the client's screen:
// they decide what belongs on a line and what does not, and nothing here needs
// a code change. It is deliberately separate from the cost recipes — that
// screen answers what a scope costs, this one answers what the line says we are
// selling, and the two are looked for at different moments.
//
// The lists ship filled in as a starting point, not as a rule. Clearing a trade
// to nothing is a legitimate answer and takes one press.

// ── Quote Settings, as two subtabs ──────────────────────────────────────────
// Stacked one above the other these were a very long screen, and the two halves
// answer different questions at different moments: what a LINE asks for, and
// what a SCOPE costs. Subtabs let each be worked on without scrolling past the
// other, and name them where they are chosen rather than only in a heading.
const QUOTE_SETTINGS_TABS = [
  { key: 'analysis', label: 'Quote Analysis', icon: '🖼️' },
  { key: 'standards', label: 'Quote Standards', icon: '🧮' },
  { key: 'lines', label: 'Quote Lines', icon: '📋' },
  { key: 'terms', label: 'Terms & Conditions', icon: '📜' },
  { key: 'art', label: 'Quotation Artwork', icon: '🖼️' },
];
function QuoteSettingsSection({ ctx, start }) {
  const [tab, setTab] = useState(start === 'standards' ? 'standards' : start === 'lines' ? 'lines' : start === 'terms' ? 'terms' : start === 'art' ? 'art' : 'analysis');
  return (
    <div className="space-y-4">
      <Tabs tabs={QUOTE_SETTINGS_TABS} active={tab} onChange={setTab} />
      {tab === 'analysis' && <AdminQuoteAnalysisTab ctx={ctx} />}
      {tab === 'standards' && <AdminQuoteDefaultsTab ctx={ctx} />}
      {tab === 'lines' && <AdminQuoteLinesTab ctx={ctx} />}
      {tab === 'terms' && <AdminQuoteTermsTab ctx={ctx} />}
      {tab === 'art' && <AdminQuoteArtTab ctx={ctx} />}
    </div>
  );
}

// WHICH SPECIFICATION FIELDS CARRY A PICTURE.
// A field answered from Supplier Finishes holds a real product — its name, its
// picture and its category. This decides which of them travel onto the client
// quote and into the presentation, per trade, because what a client should see
// a picture of differs by scope: a door's leaf finish is worth showing, its
// core is not.
// Absence means EVERY picked field shows, which is what makes this work with no
// setup; an entry is a deliberate curation and an empty one means none. Reset
// DELETES the entry rather than writing a copy of the default, so a better
// default still reaches anyone who has not curated — the same rule the spec
// lists and the software settings follow.
function AdminQuoteAnalysisTab({ ctx }) {
  const editable = ctx.canManageCollection;
  const seed = quoteSpecSeed();
  const map = ctx.quoteSpecImages || {};
  // Enumerate from the SEED, never from the sparse override map — a map that is
  // empty until someone edits something lists nothing.
  const keys = Object.keys(seed).sort((a, b) => quoteScopeLabel(a).localeCompare(quoteScopeLabel(b)));
  const setFor = (key, fields) => {
    const next = Object.assign({}, map);
    if (fields === null) delete next[key]; else next[key] = fields;
    ctx.setQuoteSpecImages(next);
  };
  return (
    <div className="space-y-3">
      <p className="text-xs text-[var(--leon-black)]/60 max-w-3xl">
        A specification field can be answered from <strong>Supplier Finishes</strong> rather than typed.
        When it is, the product&rsquo;s picture and name come with it. Tick the fields whose picture should
        reach the <strong>client quote</strong> and the <strong>presentation</strong>. A scope nobody has
        curated shows every field that has a finish picked.
      </p>
      <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl">
        The client sees the name, the picture and the category only &mdash; never the supplier, the code
        or the vendor behind it. That filter is not optional and is not switched off here.
      </p>
      {keys.map(k => {
        const fields = quoteSpecFieldsFor(k);
        const curated = Array.isArray(map[k]);
        const shown = quoteSpecImageFieldsFor(k, map, fields);
        return (
          <Collapsible key={k} id={`qimg-${k}`} title={quoteScopeLabel(k)}
            count={shown.length}
            right={
              <span className="text-[11px] text-[var(--leon-black)]/50">
                {curated ? `${shown.length} of ${fields.length} chosen` : 'all picked fields'}
              </span>
            }>
            {!fields.length ? (
              <p className="text-xs text-[var(--leon-black)]/55">
                This scope asks for no specification fields, so there is nothing to show a picture of.
                Add fields under <strong>Quote Lines</strong>.
              </p>
            ) : (
              <>
                <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-1.5">
                  {fields.map(f => (
                    <label key={f} className="flex items-center gap-2 text-xs">
                      <input type="checkbox" disabled={!editable}
                        checked={shown.indexOf(f) >= 0}
                        onChange={e => {
                          const base = shown.slice();
                          const next = e.target.checked
                            ? base.concat(base.indexOf(f) >= 0 ? [] : [f])
                            : base.filter(x => x !== f);
                          setFor(k, next);
                        }} />
                      {f}
                    </label>
                  ))}
                </div>
                {editable && (
                  <div className="flex flex-wrap gap-2 mt-3">
                    <Button size="sm" variant="ghost" onClick={() => setFor(k, fields.slice())}>All</Button>
                    <Button size="sm" variant="ghost" onClick={() => setFor(k, [])}>None</Button>
                    {curated && (
                      <Button size="sm" variant="ghost" onClick={() => setFor(k, null)}>
                        Reset to the standard
                      </Button>
                    )}
                  </div>
                )}
              </>
            )}
          </Collapsible>
        );
      })}
    </div>
  );
}

// Admin Settings -> Quote Settings -> Terms & Conditions.
// The wording that goes out on every quotation. It is VERSIONED rather than
// edited in place, because an issued quotation must keep the terms the client
// actually agreed to — re-opening Rev 00 next year has to show what was sent,
// not today's wording. So editing works on a draft of the current version and
// "Publish as version N+1" is a deliberate act; the old version stays, marked
// superseded, and any quotation that stamped it still resolves.
// Admin Settings -> Quote Settings -> Quotation Artwork.
//
// The break-page pictures and the reference list are the parts of a quotation
// that are the SAME on every job, so they are shipped rather than placed by
// hand each time — and shipped things still have to be changeable, or the next
// time LEON reshoots a kitchen the only route is a code change.
//
// SPARSE, and null means shipped: an entry exists only where the standard has
// been changed, so an improvement to the shipped set still reaches anyone who
// has not overridden it. Reset DELETES the entry rather than writing a copy of
// the default, which would freeze it by another route.
function AdminQuoteArtTab({ ctx }) {
  const art = ctx.quoteArt || { overrides: {}, references: null };
  const overrides = art.overrides || {};
  const shipped = (typeof QUOTE_ART_SCOPES !== 'undefined') ? QUOTE_ART_SCOPES : {};
  const shippedRefs = (typeof QUOTE_ART_REFERENCES !== 'undefined') ? QUOTE_ART_REFERENCES : [];
  const refs = art.references || null;
  const editable = ctx.canManageCollection;
  const [picking, setPicking] = useState(null);
  const [busy, setBusy] = useState(false);

  function write(patch) { ctx.setQuoteArt(Object.assign({}, art, patch)); }
  function setOverride(slug, url) {
    const next = Object.assign({}, overrides);
    if (url) next[slug] = url; else delete next[slug];
    write({ overrides: next });
  }
  async function upload(e, apply) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setBusy(true);
    const dataUrl = await new Promise(res => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(file); });
    const small = await shrinkSignature(dataUrl, 1600);
    setBusy(false);
    apply(small.url);
  }

  const titleOf = slug => slug.split('-').map(w => w.toUpperCase()).join(' ');
  const list = refs || shippedRefs;

  return (
    <div className="space-y-5">
      <Collapsible id="qart-about" title="How these are used" defaultOpen={false}>
        <div className="text-sm space-y-2">
          <p>Every quotation LEON sends opens each section with the SAME picture — the same kitchen
             behind KITCHEN CASEWORK, the same stone behind STONEWORK. Verified rather than assumed:
             17 of these 20 are byte-identical across the three issued decks, which is why they are
             shipped artwork rather than something to place on every job.</p>
          <p>Replacing one here changes it on every future quotation. <b>It does not change quotations
             already issued</b> — those keep the deck they went out with.</p>
        </div>
      </Collapsible>

      <div>
        <div className="flex items-center gap-2 mb-2">
          <div className="text-sm font-bold">Break-page pictures</div>
          <Badge tone="neutral">{Object.keys(shipped).length}</Badge>
          {Object.keys(overrides).length
            ? <Badge tone="brown">{Object.keys(overrides).length} changed</Badge> : null}
          <div className="flex-1" />
          {editable && Object.keys(overrides).length
            ? <Button variant="ghost" size="sm" onClick={() => write({ overrides: {} })}>Reset all to standard</Button> : null}
        </div>
        <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5 gap-3">
          {Object.keys(shipped).sort().map(slug => {
            const url = overrides[slug] || shipped[slug];
            const changed = !!overrides[slug];
            return (
              <div key={slug} className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
                <div className="relative">
                  <img src={url} alt={slug} className="w-full h-24 object-cover" />
                  {changed ? <span className="absolute top-1 left-1"><Badge tone="brown">changed</Badge></span> : null}
                </div>
                <div className="p-2">
                  <div className="text-[10px] font-bold uppercase tracking-wide truncate">{titleOf(slug)}</div>
                  {editable ? (
                    <div className="flex gap-1 mt-1">
                      <label className="text-[10px] font-semibold text-[var(--leon-brown)] cursor-pointer">
                        Replace
                        <input type="file" accept="image/*" className="hidden"
                          onChange={e => upload(e, u => setOverride(slug, u))} />
                      </label>
                      {changed ? <button className="text-[10px] text-[var(--leon-black)]/50"
                        onClick={() => setOverride(slug, null)}>Reset</button> : null}
                    </div>
                  ) : null}
                </div>
              </div>
            );
          })}
        </div>
        {busy ? <div className="text-xs mt-2">Processing…</div> : null}
      </div>

      <div>
        <div className="flex items-center gap-2 mb-2">
          <div className="text-sm font-bold">References</div>
          <Badge tone="neutral">{list.length}</Badge>
          {refs ? <Badge tone="brown">curated</Badge> : <Badge tone="neutral">standard list</Badge>}
          <div className="flex-1" />
          {editable && !refs
            ? <Button variant="outline" size="sm"
                onClick={() => write({ references: shippedRefs.map(r => ({ ...r })) })}>Edit this list</Button> : null}
          {editable && refs ? <>
            <Button variant="ghost" size="sm"
              onClick={() => write({ references: refs.concat([{ id: uid('ref'), location: '', img: '' }]) })}>+ Add</Button>
            <Button variant="ghost" size="sm" onClick={() => write({ references: null })}>Reset to standard</Button>
          </> : null}
        </div>
        <p className="text-xs text-[var(--leon-black)]/60 mb-2">
          A quotation shows a curated list if there is one, else the jobs published to Finished
          Projects, else this standard set — so a deck always carries references rather than a gap.
        </p>
        <div className="grid grid-cols-3 sm:grid-cols-6 lg:grid-cols-7 gap-2">
          {list.map((r, i) => (
            <div key={r.id || i} className="rounded-md border border-[var(--leon-line)] bg-white overflow-hidden">
              {r.img ? <img src={r.img} alt={r.location} className="w-full h-16 object-cover" />
                     : <div className="w-full h-16 bg-[var(--leon-cream)]" />}
              <div className="p-1">
                {refs && editable ? (
                  <>
                    <TextInput className="!text-[10px] !py-0.5" value={r.location || ''}
                      onChange={e => write({ references: refs.map((x, j) => j === i ? { ...x, location: e.target.value } : x) })} />
                    <div className="flex gap-1 mt-0.5">
                      <label className="text-[9px] font-semibold text-[var(--leon-brown)] cursor-pointer">
                        Picture
                        <input type="file" accept="image/*" className="hidden"
                          onChange={e => upload(e, u => write({ references: refs.map((x, j) => j === i ? { ...x, img: u } : x) }))} />
                      </label>
                      <button className="text-[9px] text-[var(--leon-red)]"
                        onClick={() => write({ references: refs.filter((_, j) => j !== i) })}>Remove</button>
                    </div>
                  </>
                ) : <div className="text-[10px] truncate">{r.location}</div>}
              </div>
            </div>
          ))}
        </div>
      </div>

      {!editable ? <div className="text-xs text-[var(--leon-black)]/60">
        You can read these here. Changing them needs Collection rights.
      </div> : null}
    </div>
  );
}

function AdminQuoteTermsTab({ ctx }) {
  const sets = ctx.quoteTerms || [];
  const current = quoteTermsCurrent(sets);
  const [draft, setDraft] = useState(null);
  const [note, setNote] = useState('');
  const editing = !!draft;
  const clauses = (draft ? draft.clauses : (current ? current.clauses : [])) || [];

  function startEdit() {
    setDraft(JSON.parse(JSON.stringify(current || makeQuoteTermsSet())));
    setNote('');
  }
  function setClause(i, patch) {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses[i] = Object.assign({}, n.clauses[i], patch);
      return n;
    });
  }
  function setBlock(ci, bi, patch) {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses[ci].body[bi] = Object.assign({}, n.clauses[ci].body[bi], patch);
      return n;
    });
  }
  function addBlock(ci) {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses[ci].body.push({ t: 'p', s: '' });
      return n;
    });
  }
  function removeBlock(ci, bi) {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses[ci].body.splice(bi, 1);
      return n;
    });
  }
  function moveClause(i, dir) {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      const j = i + dir;
      if (j < 0 || j >= n.clauses.length) return d;
      const t = n.clauses[i]; n.clauses[i] = n.clauses[j]; n.clauses[j] = t;
      return n;
    });
  }
  function addClause() {
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses.push({ n: String(n.clauses.length + 1), title: '', body: [{ t: 'p', s: '' }] });
      return n;
    });
  }
  function removeClause(i) {
    if (!window.confirm('Remove this clause from the draft?')) return;
    setDraft(d => {
      const n = JSON.parse(JSON.stringify(d));
      n.clauses.splice(i, 1);
      return n;
    });
  }
  function publish() {
    const next = makeQuoteTermsSet({
      id: uid('qterms'),
      version: (current ? qnum(current.version) : 0) + 1,
      createdBy: ctx.currentUserName,
      note: typeof note === 'string' ? note : '',
      clauses: draft.clauses.map((c, i) => ({ n: String(i + 1), title: c.title, body: c.body })),
    });
    // The previous version is kept and simply stops being current: a quotation
    // that stamped it must still resolve to the wording it was issued with.
    ctx.setQuoteTerms(prev => [next].concat(prev || []));
    setDraft(null);
    setNote('');
  }

  const editable = ctx.canManageCollection;
  return (
    <div className="space-y-4">
      <Collapsible id="qterms-about" title="How these are used" defaultOpen={false}>
        <div className="text-sm space-y-2">
          <p>These clauses print at the back of every quotation deck, paginated automatically —
             clause 4 alone runs to nineteen blocks, so pages are filled by measured height
             rather than a fixed number of clauses.</p>
          <p><b>Editing does not change quotations already issued.</b> Publishing raises a new
             version; a quotation stamps the version it went out with, so re-opening an old
             revision shows the wording the client agreed to.</p>
          <p className="text-xs text-[var(--leon-black)]/60">
             The 15 standard clauses were transcribed verbatim from the issued 3-4 Seagrave
             deck. The source marks no bullets — every line there is a plain paragraph — so
             whether a line reads as a paragraph, a sub-heading or a list item was inferred
             and is corrected here.</p>
        </div>
      </Collapsible>

      <div className="flex items-center gap-3 flex-wrap">
        <Badge tone="brown">{current ? 'Version ' + current.version : 'No terms yet'}</Badge>
        {current && current.createdDate
          ? <span className="text-xs text-[var(--leon-black)]/60">
              published {fmtDate(current.createdDate)}{current.createdBy ? ' by ' + current.createdBy : ''}
            </span>
          : null}
        <span className="text-xs text-[var(--leon-black)]/60">{clauses.length} clauses</span>
        <div className="flex-1" />
        {editable && !editing
          ? <Button variant="outline" onClick={startEdit}>Edit</Button>
          : null}
        {editing ? <>
          <TextInput value={note} onChange={e => setNote(e.target.value)} placeholder="What changed, in a line" className="w-64" />
          <Button variant="primary" onClick={publish}>
            Publish as version {(current ? qnum(current.version) : 0) + 1}
          </Button>
          <Button variant="ghost" onClick={() => { setDraft(null); setNote(''); }}>Cancel</Button>
        </> : null}
      </div>
      {!editable
        ? <div className="text-xs text-[var(--leon-black)]/60">
            You can read the terms here. Changing them needs Collection rights.
          </div>
        : null}

      {clauses.map((c, i) => (
        <div key={i} className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-2">
          <div className="flex items-center gap-2">
            <span className="text-xs font-bold w-6 text-[var(--leon-black)]/50">{i + 1}.</span>
            {editing
              ? <TextInput value={c.title} onChange={e => setClause(i, { title: e.target.value })}
                  placeholder="Clause title" className="flex-1" />
              : <span className="font-bold text-sm flex-1">{c.title}</span>}
            {editing ? <>
              <Button variant="ghost" size="sm" onClick={() => moveClause(i, -1)} title="Move up">↑</Button>
              <Button variant="ghost" size="sm" onClick={() => moveClause(i, 1)} title="Move down">↓</Button>
              <Button variant="danger" size="sm" onClick={() => removeClause(i)} title="Remove clause">✕</Button>
            </> : null}
          </div>
          <div className="space-y-1 pl-6">
            {(c.body || []).map((b, bi) => (
              <div key={bi} className="flex items-start gap-2">
                {editing ? (
                  <Select value={b.t} onChange={e => setBlock(i, bi, { t: e.target.value })} className="w-28">
                    <option value="p">Paragraph</option>
                    <option value="h">Sub-heading</option>
                    <option value="li">List item</option>
                  </Select>
                ) : (
                  <span className="text-[10px] uppercase tracking-wide w-16 pt-1 text-[var(--leon-black)]/40">
                    {b.t === 'h' ? 'heading' : ''}
                  </span>
                )}
                {editing
                  ? <TextArea value={b.s} onChange={e => setBlock(i, bi, { s: e.target.value })} rows={b.s.length > 140 ? 3 : 1} className="flex-1" />
                  : <span className={'text-sm flex-1 ' + (b.t === 'h' ? 'font-bold' : b.t === 'li' ? 'pl-3' : '')}>
                      {b.t === 'li' ? '• ' : ''}{b.s}
                    </span>}
                {editing ? <Button variant="danger" size="sm" onClick={() => removeBlock(i, bi)} title="Remove line">✕</Button> : null}
              </div>
            ))}
            {editing ? <Button variant="ghost" size="sm" onClick={() => addBlock(i)}>+ Add a line</Button> : null}
          </div>
        </div>
      ))}
      {editing ? <Button variant="outline" size="sm" onClick={addClause}>+ Add a clause</Button> : null}

      {sets.length > 1 ? (
        <Collapsible id="qterms-history" title="Earlier versions" count={sets.length - 1} defaultOpen={false}>
          <div className="space-y-1 text-sm">
            {sets.filter(x => !current || x.id !== current.id).map(x => (
              <div key={x.id} className="flex items-center gap-3">
                <Badge tone="neutral">v{x.version}</Badge>
                <span className="text-xs text-[var(--leon-black)]/60">
                  {fmtDate(x.createdDate)}{x.createdBy ? ' \u00b7 ' + x.createdBy : ''}
                </span>
                <span className="text-xs">{x.note || ''}</span>
                <span className="text-xs text-[var(--leon-black)]/40">{(x.clauses || []).length} clauses</span>
              </div>
            ))}
          </div>
          <p className="text-xs text-[var(--leon-black)]/60 mt-2">
            Kept, not deleted — a quotation issued against one of these still has to resolve
            to the wording it went out with.
          </p>
        </Collapsible>
      ) : null}
    </div>
  );
}

function AdminQuoteLinesTab({ ctx }) {
  const editable = ctx.canManageCollection;
  const seed = quoteSpecSeed();
  const specs = ctx.quoteSpecs || {};
  const keys = Object.keys(seed).sort((a, b) => quoteScopeLabel(a).localeCompare(quoteScopeLabel(b)));
  const changed = keys.filter(k => specs[k]);
  const total = keys.reduce((n, k) => n + (specs[k] || seed[k] || []).length, 0);

  return (
    <div className="space-y-4">
      <div>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          What a quoted line of each trade asks for &mdash; the specification that travels beside the
          price, onto the client&rsquo;s quotation and onto the deck. Add a field, remove one, or put them
          in the order you read them in.
        </p>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl mt-1.5">
          <b>These lists are a starting point, not a rule.</b> A field that ends up blank on every line is
          worse than no field, so remove freely &mdash; and clearing a trade to nothing is a perfectly
          good answer.
        </p>
        <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
          {total} field{total === 1 ? '' : 's'} over {keys.length} trades
          {changed.length ? ` · ${changed.length} changed from the standard` : ' · all following the standard'}
        </p>
      </div>

      {!editable && <LockedNotice label="You can read the quotation line fields but not change them." />}

      <div className="space-y-3">
        {keys.map(k => (
          <Collapsible key={k} id={`qlines-${k}`} title={quoteScopeLabel(k)}
            right={
              <span className="flex items-center gap-2 text-xs">
                {specs[k] && <Badge tone="yellow">changed</Badge>}
                <span className="text-[var(--leon-black)]/45 tabular-nums">
                  {(specs[k] || seed[k] || []).length} fields
                </span>
              </span>
            }>
            <QuoteSpecFieldsEditor ctx={ctx} scopeKey={k} editable={editable} standalone />
          </Collapsible>
        ))}
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3">
        <p className="text-sm font-semibold mb-1">Where these show up</p>
        <p className="text-xs text-[var(--leon-black)]/60">
          On a quotation, open a line&rsquo;s <b>&middot;&middot;&middot;</b> panel &mdash; the fields for that
          trade are under the scope&rsquo;s name. They travel to the client quotation and onto the
          presentation, so what is asked here is what a client eventually reads.
        </p>
      </div>
    </div>
  );
}

function AdminQuoteDefaultsTab({ ctx }) {
  const editable = ctx.canManageCollection;
  const recipes = ctx.quoteRecipes || {};
  const seed = quoteRecipeSeed();
  // The UNION of the two libraries. A trade can have a specification list and
  // no cost recipe (LVT and Rubber do), and a card that only exists for one of
  // them leaves the other with nowhere to be edited.
  // Union with the SEED, not with `ctx.quoteSpecs` — that map is sparse and
  // holds only overrides, so it is empty until someone edits something and
  // unioning with it silently dropped LVT and Rubber.
  const keys = Array.from(new Set(Object.keys(recipes).concat(Object.keys(quoteSpecSeed())))).sort();

  function set(scopeKey, field, value) {
    if (!editable) return;
    ctx.setQuoteRecipes(prev => {
      const next = Object.assign({}, prev);
      next[scopeKey] = Object.assign({}, next[scopeKey], { [field]: value });
      return next;
    });
  }
  function reset(scopeKey) {
    if (!editable) return;
    ctx.setQuoteRecipes(prev => {
      const next = Object.assign({}, prev);
      next[scopeKey] = Object.assign({}, seed[scopeKey] || {});
      return next;
    });
  }
  // Which fields matter for a given scope. A stone scope has no container
  // capacity and a casework scope has no slab yield; showing every field for
  // every scope is how a form stops being readable.
  function fieldsFor(r) {
    return QUOTE_RECIPE_FIELDS.filter(f => {
      if (f.key === 'pieceLengthIn') return r.costDriver === 'piece';
      if (f.key === 'slabYield' || f.key === 'slabWastePct') return r.matBasis === 'slab';
      if (f.key === 'containerCapacity') return r.matBasis !== 'slab';
      return true;
    });
  }

  return (
    <div className="space-y-4">
      <div>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          What a new quoted scope starts from &mdash; how it is counted, how many fit a container, what
          the three freight legs cost, and the overhead and waste carried on it. Every one of these was
          read out of the Revere Street workbook, where it sat inside a formula; this is the first time
          they have been visible in one place.
        </p>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl mt-1.5">
          <b>A quotation copies these when the scope is created</b>, the same way a project scope copies
          its stage template. Changing a figure here never reprices a draft that already exists.
        </p>
      </div>

      {!editable && <LockedNotice label="You can read the quote defaults but not change them." />}

      <div className="space-y-3">
        {keys.map(k => {
          const r = recipes[k] || {};
          const base = seed[k] || {};
          const changed = Object.keys(base).some(f => String(r[f]) !== String(base[f]));
          return (
            <Collapsible key={k} id={`qrec-${k}`} title={quoteScopeLabel(k)}
              right={
                <span className="flex items-center gap-2 text-xs">
                  {changed && <Badge tone="yellow">changed from standard</Badge>}
                  <span className="text-[var(--leon-black)]/45">{r.uom || '—'}</span>
                </span>
              }>
              <EditLock canEdit={editable} hint="Locked — press Edit to change this scope's defaults.">
                <div className="grid sm:grid-cols-3 lg:grid-cols-4 gap-3">
                  {fieldsFor(r).map(f => (
                    <Field key={f.key} label={f.label} hint={f.hint}>
                      {f.kind === 'text' ? (
                        <TextInput value={r[f.key] || ''} onChange={e => set(k, f.key, e.target.value)} />
                      ) : f.kind === 'driver' ? (
                        <Select value={r.costDriver || 'qty'} onChange={e => set(k, 'costDriver', e.target.value)}>
                          {QUOTE_COST_DRIVERS.map(d => <option key={d.key} value={d.key}>{d.label}</option>)}
                        </Select>
                      ) : f.kind === 'pct' ? (
                        <QPct w="w-20" value={r[f.key]} onChange={v => set(k, f.key, v)} />
                      ) : (
                        <QNum w="w-28" prefix={f.kind === 'money' ? '$' : ''} suffix={f.suffix}
                          value={r[f.key]} onChange={v => set(k, f.key, v)} />
                      )}
                    </Field>
                  ))}
                </div>
              </EditLock>
              {changed && editable && (
                <button onClick={() => reset(k)} className="mt-2 text-xs font-semibold text-[var(--leon-brown)]">
                  Reset to the standard
                </button>
              )}
            </Collapsible>
          );
        })}
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] p-3">
        <p className="text-sm font-semibold mb-1">Margin tiers</p>
        <p className="text-xs text-[var(--leon-black)]/60">
          {QUOTE_MARGIN_TIERS.map(t => `${t.label} ${Math.round(t.rate * 100)}%`).join(' · ')} &mdash; chosen
          on the job and overridable per scope. Commission is {Math.round(0.03 * 100)}% on everything except
          countertops, which are {Math.round(0.06 * 100)}%.
        </p>
      </div>
    </div>
  );
}

// ── The casework library, in LEON Collection ───────────────────────────────
// Same treatment as the door library: the panels LEON Casework renders under
// Casework Settings, against the same records, gated on `casework.library`
// rather than Collection access.
const CASEWORK_LIBRARY_SECTIONS = [
  { key: 'cabLibrary', label: 'Cabinet Library', hint: "The modules LEON builds and the widths each is made in — read from the company's own US cabinet library." },
  { key: 'appliances', label: 'Appliances', hint: 'What each appliance needs from the casework, and how far the data can be trusted.' },
  { key: 'components', label: 'Components', hint: 'Fillers, scribes, finished ends, mouldings.' },
  { key: 'rules', label: 'Planning Rules', hint: 'The rules a warning cites, and the construction defaults a new cabinet starts from.' },
  { key: 'types', label: 'Cabinet Types', hint: 'The types this company builds to.' },
  { key: 'global', label: 'Global Library', hint: 'Types shared across every job.' },
  { key: 'construction', label: 'Construction', hint: 'Material thicknesses, reveals and joinery.' },
  { key: 'hardware', label: 'Hardware', hint: 'Hinges, slides, pulls and the sets they form.' },
];
function CaseworkLibraryCollectionTab({ ctx }) {
  const [sec, setSec] = useState('cabLibrary');
  const system = 'Imperial';
  const editable = ctx.canEditCaseworkLibrary !== false && ctx.canEdit('softwares');
  const active = CASEWORK_LIBRARY_SECTIONS.find(x => x.key === sec) || CASEWORK_LIBRARY_SECTIONS[0];
  // The job-level panels need a project; from the Collection there is none, and
  // that is the honest state — these are the LIBRARY halves of them.
  const P = {
    cabLibrary: typeof CwLibraryModulesPanel === 'function' ? <CwLibraryModulesPanel ctx={ctx} system={system} /> : null,
    appliances: typeof CwLibraryAppliancesPanel === 'function' ? <CwLibraryAppliancesPanel ctx={ctx} /> : null,
    components: typeof CwLibraryComponentsPanel === 'function' ? <CwLibraryComponentsPanel ctx={ctx} /> : null,
    rules: typeof CwLibraryRulesPanel === 'function' ? <CwLibraryRulesPanel ctx={ctx} /> : null,
    types: typeof CwCabinetTypesPanel === 'function' ? <CwCabinetTypesPanel ctx={ctx} project={null} system={system} editable={editable} /> : null,
    global: typeof CwGlobalLibrary === 'function' ? <CwGlobalLibrary ctx={ctx} project={null} system={system} editable={editable} /> : null,
    construction: typeof CwConstructionPanel === 'function' ? <CwConstructionPanel ctx={ctx} project={null} system={system} editable={editable} /> : null,
    hardware: typeof CwHardwarePanel === 'function' ? <CwHardwarePanel ctx={ctx} project={null} system={system} editable={editable} /> : null,
  }[sec];
  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
        The standards every cabinet is built to. This is the <b>same library</b> LEON Casework reads under
        Casework Settings &mdash; one set of records, edited in either place.
      </p>
      {!editable && (
        <LockedNotice label="You can read the casework library but not change it. Editing is held by Admin, Senior Associate, Associates and the Production Director." />
      )}
      <div className="flex flex-wrap gap-1">
        {CASEWORK_LIBRARY_SECTIONS.map(x => (
          <button key={x.key} onClick={() => setSec(x.key)}
            className={`subtab-btn px-2 py-1 rounded text-[12px] ${sec === x.key
              ? 'bg-[var(--leon-brown)] text-white font-semibold' : 'hover:bg-[var(--leon-cream)]'}`}>
            {x.label}
          </button>
        ))}
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/45">{active.hint}</p>
      <div className="pt-1">{P || <EmptyState text={`${active.label} could not be loaded.`} />}</div>
    </div>
  );
}

// ── The door library, in LEON Collection ───────────────────────────────────
// The same panels LEON Doors renders under Door Settings, against the same
// persisted `doorLibrary` — one set of records reached from two places, the way
// the Tariff Library already is. Editing here changes the standard every future
// door inherits, which is why it is gated on the same capability the tool uses
// (`doors.library`) and not on Collection access alone: those are different
// authorities and the narrower one has to win.
const DOOR_LIBRARY_SECTIONS = [
  { key: 'models', label: 'LEON Models', hint: "LEON's published door catalog — read only, the file is the source." },
  { key: 'types', label: 'Door Types', hint: 'The standards a door inherits from.' },
  { key: 'global', label: 'Global Library', hint: 'Types shared across every job.' },
  { key: 'frames', label: 'Frames', hint: 'Jamb profiles and how each is built.' },
  { key: 'trims', label: 'Trims', hint: 'Casing profiles: design, size and joint.' },
  { key: 'rules', label: 'Opening Rules', hint: 'Leaf to frame to rough opening.' },
  { key: 'designs', label: 'Leaf Designs', hint: 'Panels, grooves and rails.' },
  { key: 'hardware', label: 'Hardware', hint: 'Ironmongery and hardware sets.' },
];
function DoorLibraryCollectionTab({ ctx }) {
  const [sec, setSec] = useState('frames');
  const system = 'Imperial';
  const editable = ctx.canEditDoorLibrary !== false && ctx.canEdit('softwares');
  const active = DOOR_LIBRARY_SECTIONS.find(x => x.key === sec) || DOOR_LIBRARY_SECTIONS[0];
  // Every panel lives in softwares/doors.jsx, which loads BEFORE app.jsx — the
  // guard turns a load-order change into a named notice rather than a blank
  // tab, the same way OfficeHome guards its editors.
  const P = {
    models: typeof DoorModelsPanel === 'function' ? <DoorModelsPanel ctx={ctx} /> : null,
    types: typeof DoorTypesPanel === 'function' ? <DoorTypesPanel ctx={ctx} project={null} system={system} editable={editable} /> : null,
    global: typeof DoorGlobalLibrary === 'function' ? <DoorGlobalLibrary ctx={ctx} system={system} editable={editable} /> : null,
    frames: typeof DoorFramesPanel === 'function' ? <DoorFramesPanel ctx={ctx} system={system} editable={editable} /> : null,
    trims: typeof DoorTrimsPanel === 'function' ? <DoorTrimsPanel ctx={ctx} system={system} editable={editable} /> : null,
    rules: typeof DoorRulesPanel === 'function' ? <DoorRulesPanel ctx={ctx} system={system} editable={editable} /> : null,
    designs: typeof DoorDesignsPanel === 'function' ? <DoorDesignsPanel ctx={ctx} system={system} editable={editable} /> : null,
    hardware: typeof DoorHardwarePanel === 'function' ? <DoorHardwarePanel ctx={ctx} editable={editable} /> : null,
  }[sec];

  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
        The standards every door is built to. This is the <b>same library</b> LEON Doors reads under
        Door Settings &mdash; one set of records, edited in either place.
      </p>
      {!editable && (
        <LockedNotice label="You can read the door library but not change it. Editing is held by Admin, Senior Associate, Associates and the Production Director." />
      )}
      <div className="flex flex-wrap gap-1">
        {DOOR_LIBRARY_SECTIONS.map(x => (
          <button key={x.key} onClick={() => setSec(x.key)}
            className={`subtab-btn px-2 py-1 rounded text-[12px] ${sec === x.key
              ? 'bg-[var(--leon-brown)] text-white font-semibold'
              : 'hover:bg-[var(--leon-cream)]'}`}>
            {x.label}
          </button>
        ))}
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/45">{active.hint}</p>
      <div className="pt-1">{P || <EmptyState text={`${active.label} could not be loaded.`} />}</div>
    </div>
  );
}

// The TILE and WINDOW libraries, mirrored the same way. Standing data every
// future room or window inherits belongs with the rest of the standing data,
// not only inside the tool that draws with it. Same components, same records,
// two doors — the pattern the Tariff, Door and Casework libraries already use.
function TileLibraryCollectionTab({ ctx }) {
  const editable = ctx.canManageCollection && ctx.canEdit('softwares');
  const lib = typeof surfLib === 'function' ? surfLib(ctx) : null;
  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
        Surface types and pattern presets &mdash; the standards a room type is set out from. This is the
        <b> same library</b> LEON Tiles and Flooring reads under Tile Settings; one set of records, edited
        in either place.
      </p>
      {!editable && <LockedNotice label="You can read the tile library but not change it." />}
      {typeof SurfSetupTab === 'function' && lib
        ? <SurfSetupTab ctx={ctx} lib={lib} sys="Imperial" canEditLib={editable} />
        : <EmptyState text="The tile library could not be loaded." />}
    </div>
  );
}

const FEN_LIBRARY_SECTIONS = [
  { key: 'systems', label: 'Manufacturers & Systems',
    hint: 'The systems a window may be built from. A system with no verified manufacturer CAD is marked as such rather than drawn from invented geometry.' },
  { key: 'profiles', label: 'Profile Library',
    hint: 'The extrusion sections themselves — frame, sash, mullion, transom — with the geometry each one actually publishes.' },
];
function WindowLibraryCollectionTab({ ctx }) {
  const [sec, setSec] = useState('systems');
  const editable = ctx.canManageCollection && ctx.canEdit('softwares');
  // useFenLibrary is the module's own subscription hook — the fenestration
  // library is a module-level store mirrored into persisted state, so reading
  // it any other way gets a snapshot that stops updating.
  const lib = typeof useFenLibrary === 'function' ? useFenLibrary() : null;
  const active = FEN_LIBRARY_SECTIONS.find(x => x.key === sec) || FEN_LIBRARY_SECTIONS[0];
  const P = {
    systems: typeof FenSystemsPanel === 'function' ? <FenSystemsPanel ctx={ctx} lib={lib} editable={editable} /> : null,
    profiles: typeof FenProfilesPanel === 'function' ? <FenProfilesPanel ctx={ctx} lib={lib} editable={editable} /> : null,
  }[sec];
  return (
    <div className="space-y-3">
      <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
        The systems and profiles every window is built from. This is the <b>same library</b> LEON Windows
        reads under Window Settings &mdash; one set of records, edited in either place.
      </p>
      {!editable && <LockedNotice label="You can read the window library but not change it." />}
      <div className="flex flex-wrap gap-1">
        {FEN_LIBRARY_SECTIONS.map(x => (
          <button key={x.key} onClick={() => setSec(x.key)}
            className={`subtab-btn px-2 py-1 rounded text-[12px] ${sec === x.key
              ? 'bg-[var(--leon-brown)] text-white font-semibold'
              : 'hover:bg-[var(--leon-cream)]'}`}>
            {x.label}
          </button>
        ))}
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/45">{active.hint}</p>
      <div className="pt-1">{P || <EmptyState text={`${active.label} could not be loaded.`} />}</div>
    </div>
  );
}

const LEON_COLLECTION_TABS = [
  { key: 'scopes', label: 'Scopes & Selections', icon: '📐' },
  { key: 'finishes', label: 'Supplier Finishes', icon: '🎨' },
  { key: 'materials', label: 'Scope Documents', icon: '📑' },
  { key: 'renders', label: 'Render Library', icon: '🖼️' },
  // The DOOR LIBRARY — models, types, frames, trims, opening rules, leaf
  // designs and hardware. It is standing data every future door inherits, so it
  // belongs with the rest of the standing data and not only inside the tool
  // that draws with it. Same components, same records, two doors — the pattern
  // the Tariff Library already uses.
  { key: 'doors', label: 'Door Library', icon: '🚪' },
  { key: 'casework', label: 'Casework Library', icon: '🧱' },
  { key: 'tiles', label: 'Tile Library', icon: '🧱' },
  { key: 'windows', label: 'Window Library', icon: '🪟' },
  // The TARIFF LIBRARY only — HTS codes and the rates that have been in force
  // for them. That is reference data everyone may read, which is why it is here
  // and open. The rest of Trade Compliance (tariff LINES, the dashboard and the
  // exposure report) carries duty paid and project cost, so it stays in the
  // operational hub behind its own permission.
  { key: 'tariffs', label: 'Tariff Library', icon: '🛃' },
  { key: 'salesTax', label: 'Sales Tax', icon: '🏛️' },
  // Everything a quotation is set up from: what a line asks for, and what a
  // scope costs. One section, because "quote settings" is the one thing anyone
  // comes here looking for.
  { key: 'quoteSettings', label: 'Quote Settings', icon: '🧮' },
  { key: 'leadTimes', label: 'Lead Times', icon: '🗓️' },
  { key: 'holidays', label: 'Holidays', icon: '🎌' },
  { key: 'demo', label: 'Demo Data', icon: '🧪' },
];

// LEON Collection had grown into one long scroll of six unrelated things.
// Split into tabs along the lines of what someone is actually here to do:
// define what we sell, browse what our suppliers offer, manage our own
// material specs, tune scheduling defaults, or change company settings.
const ACCOUNTING_TABS = [
  { key: 'calendar', label: 'Financial Calendar', icon: '💰' },
  { key: 'entries', label: 'Company Income & Expenses', icon: '🏢' },
  { key: 'cards', label: 'Credit Cards', icon: '💳' },
  { key: 'progress', label: 'Job Progress', icon: '📈' },
];

// Company-wide cash planning. Everything here is FORECAST until the money
// actually moves — see buildCashEvents (lib.jsx) for where each row comes from.
function AccountingHub({ ctx }) {
  const [tab, setTab] = useHubSection(ctx, 'accounting', 'calendar');
  return (
    <div>
      <h1 className="text-2xl font-bold mb-1">Accounting</h1>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        What is expected in and out, week by week, across every project &mdash; plus company costs
        that sit outside any job. Planned amounts are estimates until payment is recorded; once it is,
        the date locks to when the money actually moved.
      </p>
      <Tabs tabs={ACCOUNTING_TABS} active={tab} onChange={setTab} />
      <div className="pt-4">
        {tab === 'calendar' && <FinancialCalendarView ctx={ctx} />}
        {tab === 'entries' && <CashEntriesView ctx={ctx} />}
        {tab === 'cards' && <CreditCardsView ctx={ctx} />}
        {tab === 'progress' && <JobProgressView ctx={ctx} />}
      </div>
    </div>
  );
}

// Job Progress — the accounting seat's view of the physical work.
// The columns are deliberately paired: how far the WORK has got, next to how
// far the MONEY has got on the same scope. A scope 90% produced and 30% billed
// is the row that should start a conversation, and this is the only place in
// the app where those two numbers sit side by side.
function JobProgressView({ ctx }) {
  const [mode, setMode] = useState('production');
  const [fProject, setFProject] = useState('all');
  const [fClient, setFClient] = useState('all');
  const [fFamily, setFFamily] = useState('all');
  const [fDept, setFDept] = useState('all');
  const [fStatus, setFStatus] = useState('all');
  const [onlyOpen, setOnlyOpen] = useState(true);
  const [q, setQ] = useState('');
  const [sortKey, setSortKey] = useState('gap');

  const all = useMemo(() => buildJobProgressRows(ctx.projects, ctx.accounts, mode),
    [ctx.projects, ctx.accounts, mode]);
  const clients = [...new Set(all.map(r => r.clientName).filter(Boolean))].sort();
  const families = [...new Set(all.map(r => r.family).filter(Boolean))].sort();
  const statuses = [...new Set(all.map(r => r.status).filter(Boolean))].sort();

  const rows = useMemo(() => {
    const ql = q.trim().toLowerCase();
    let out = all.filter(r =>
      (fProject === 'all' || r.projectId === fProject) &&
      (fClient === 'all' || r.clientName === fClient) &&
      (fFamily === 'all' || r.family === fFamily) &&
      (fDept === 'all' || r.department === fDept) &&
      (fStatus === 'all' || r.status === fStatus) &&
      (!onlyOpen || !['Completed Job', 'Lost Job'].includes(r.projectStatus)) &&
      (!ql || [r.projectName, r.scopeName, r.clientName, r.crew].some(v => (v || '').toLowerCase().includes(ql)))
    );
    const billedPct = r => (r.salesValue ? (r.paid / r.salesValue) * 100 : 0);
    const cmp = {
      gap: (a, b) => ((b.pct || 0) - billedPct(b)) - ((a.pct || 0) - billedPct(a)),
      project: (a, b) => textAsc(a.projectName + a.scopeName, b.projectName + b.scopeName),
      pct: (a, b) => (b.pct || 0) - (a.pct || 0),
      value: (a, b) => b.salesValue - a.salesValue,
    }[sortKey];
    return [...out].sort(cmp);
  }, [all, fProject, fClient, fFamily, fDept, fStatus, onlyOpen, q, sortKey]);

  const totals = rows.reduce((t, r) => ({
    value: t.value + r.salesValue, cost: t.cost + r.committed, paid: t.paid + r.paid,
    pct: t.pct + (r.pct || 0), counted: t.counted + (r.pct == null ? 0 : 1),
  }), { value: 0, cost: 0, paid: 0, pct: 0, counted: 0 });
  const avgPct = totals.counted ? totals.pct / totals.counted : 0;

  function exportCsv() {
    downloadCsv(`job-progress-${mode}-${todayISO()}`, [
      { key: 'projectNumber', label: 'Project #' }, { key: 'projectName', label: 'Project' },
      { key: 'clientName', label: 'Client' }, { key: 'department', label: 'Department' },
      { key: 'scopeName', label: 'Scope' }, { key: 'family', label: 'Scope Family' },
      { key: 'crew', label: mode === 'installation' ? 'Crew / Installer' : 'Vendor' },
      { key: 'status', label: 'Status' }, { key: 'pct', label: '% Complete' },
      { key: 'started', label: 'Started', type: 'date' }, { key: 'completed', label: 'Completed', type: 'date' },
      { key: 'openPunch', label: 'Open Punch' },
      { key: 'salesValue', label: 'Scope Value', type: 'money' },
      { key: 'committed', label: 'Cost Committed', type: 'money' },
      { key: 'paid', label: 'Cost Paid', type: 'money' },
    ], rows.map(r => ({ ...r, pct: r.pct == null ? '' : Math.round(r.pct) })));
  }

  const isInstall = mode === 'installation';
  return (
    <div>
      <div className="flex items-start justify-between gap-3 flex-wrap mb-3">
        <p className="text-sm text-[var(--leon-black)]/50 max-w-3xl">
          Where the physical work stands on every scope, next to the money booked against it.
          Read the two percentages together: work well ahead of billing is revenue you have earned
          and not asked for; cost well ahead of work is money out the door early.
        </p>
        <Button variant="outline" onClick={exportCsv}>Export CSV</Button>
      </div>

      <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit mb-3">
        {[{ k: 'production', l: '🏭 Production' }, { k: 'installation', l: '🔧 Installation' }].map(o => (
          <button key={o.k} onClick={() => { setMode(o.k); setFStatus('all'); }}
            className={`px-3 py-1.5 rounded-md text-sm font-semibold ${mode === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
        ))}
      </div>

      <div className="flex items-center gap-2 flex-wrap mb-3">
        <Select value={fProject} onChange={e => setFProject(e.target.value)} className="!w-56 !py-1 !text-xs">
          <option value="all">All projects</option>
          {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
        </Select>
        <Select value={fClient} onChange={e => setFClient(e.target.value)} className="!w-44 !py-1 !text-xs">
          <option value="all">All clients</option>
          {clients.map(c => <option key={c} value={c}>{c}</option>)}
        </Select>
        <Select value={fFamily} onChange={e => setFFamily(e.target.value)} className="!w-44 !py-1 !text-xs">
          <option value="all">All scope families</option>
          {families.map(f => <option key={f} value={f}>{f}</option>)}
        </Select>
        <Select value={fDept} onChange={e => setFDept(e.target.value)} className="!w-36 !py-1 !text-xs">
          <option value="all">Both departments</option>
          {DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
        </Select>
        <Select value={fStatus} onChange={e => setFStatus(e.target.value)} className="!w-44 !py-1 !text-xs">
          <option value="all">Any status</option>
          {statuses.map(st => <option key={st} value={st}>{st}</option>)}
        </Select>
        <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search scope, crew, vendor…" className="!w-52 !py-1 !text-xs" />
        <label className="flex items-center gap-1.5 text-xs text-[var(--leon-black)]/60 cursor-pointer">
          <input type="checkbox" checked={onlyOpen} onChange={e => setOnlyOpen(e.target.checked)} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
          Live jobs only
        </label>
        <div className="flex-1" />
        <Select value={sortKey} onChange={e => setSortKey(e.target.value)} className="!w-52 !py-1 !text-xs">
          <option value="gap">Sort: biggest work/paid gap</option>
          <option value="project">Sort: project &amp; scope</option>
          <option value="pct">Sort: most complete</option>
          <option value="value">Sort: largest value</option>
        </Select>
      </div>

      <div className="grid sm:grid-cols-4 gap-3 mb-3">
        <StatBox label="Scopes shown" value={String(rows.length)} />
        <StatBox label={`Average ${isInstall ? 'installation' : 'production'} complete`} value={`${Math.round(avgPct)}%`} />
        <StatBox label="Scope value" value={fmtMoney(totals.value)} />
        <StatBox label="Cost paid / committed" value={`${fmtMoney(totals.paid)} / ${fmtMoney(totals.cost)}`} />
      </div>

      {rows.length === 0 ? <EmptyState text="No scopes match these filters." /> : (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
          <table className="w-full text-xs" style={{ minWidth: 1080 }}>
            <thead>
              <tr className="border-b border-[var(--leon-line)] text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50">
                <th className="px-3 py-2">Project / Scope</th>
                <th className="px-2 py-2">Client</th>
                <th className="px-2 py-2">{isInstall ? 'Crew' : 'Vendor'}</th>
                <th className="px-2 py-2">Status</th>
                <th className="px-2 py-2 w-40">Work complete</th>
                <th className="px-2 py-2">Started</th>
                <th className="px-2 py-2">{isInstall ? 'Open punch' : 'Completed'}</th>
                <th className="px-2 py-2 text-right">Scope value</th>
                <th className="px-2 py-2 text-right">Cost paid</th>
              </tr>
            </thead>
            <tbody>
              {rows.map(r => {
                const pct = r.pct == null ? null : Math.round(r.pct);
                const paidPct = r.salesValue ? Math.round((r.paid / r.salesValue) * 100) : 0;
                return (
                  <tr key={r.id} className="border-b border-[var(--leon-line)] last:border-0 hover:bg-[var(--leon-cream)]/50">
                    <td className="px-3 py-2">
                      <button onClick={() => ctx.goProject(r.projectId)} className="font-semibold hover:underline text-left">{r.scopeName}</button>
                      <p className="text-[10px] text-[var(--leon-black)]/45">{r.projectNumber} &middot; {r.projectName}</p>
                    </td>
                    <td className="px-2 py-2">{r.clientName || '—'}</td>
                    <td className="px-2 py-2 max-w-[10rem] truncate" title={r.crew}>{r.crew || '—'}</td>
                    <td className="px-2 py-2 whitespace-nowrap">
                      <span className="px-1.5 py-0.5 rounded text-[10px] font-semibold text-white"
                            style={{ background: scheduleColor(pct === 100 ? 'Complete' : pct ? 'In Progress' : 'Not Started') }}>
                        {r.status}
                      </span>
                    </td>
                    <td className="px-2 py-2">
                      {pct == null ? <span className="text-[var(--leon-black)]/30">—</span> : (
                        <div>
                          {/* Work above, money below, on the same scale — the
                              gap between the two bars IS the finding. */}
                          <div className="h-2 rounded-full bg-[var(--leon-line)] overflow-hidden">
                            <div className="h-full rounded-full" style={{ width: `${pct}%`, background: scheduleGradient(pct === 100 ? 'Complete' : 'In Progress') }} />
                          </div>
                          <div className="h-1.5 rounded-full bg-[var(--leon-line)] overflow-hidden mt-0.5">
                            <div className="h-full rounded-full" style={{ width: `${Math.min(100, paidPct)}%`, background: scheduleGradient('Delayed') }} />
                          </div>
                          <p className="text-[10px] text-[var(--leon-black)]/45 mt-0.5 tabular-nums">
                            {pct}% work &middot; {paidPct}% cost paid
                          </p>
                        </div>
                      )}
                    </td>
                    <td className="px-2 py-2 whitespace-nowrap">{r.started ? fmtDate(r.started) : '—'}</td>
                    <td className="px-2 py-2 whitespace-nowrap">
                      {isInstall
                        ? (r.openPunch ? <span className="text-[var(--leon-red)] font-semibold">{r.openPunch} open</span> : <span className="text-[var(--leon-black)]/35">none</span>)
                        : (r.completed ? fmtDate(r.completed) : '—')}
                    </td>
                    <td className="px-2 py-2 text-right font-semibold tabular-nums">{fmtMoney(r.salesValue)}</td>
                    <td className="px-2 py-2 text-right tabular-nums">{fmtMoney(r.paid)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-2">
        Work % comes from the {isInstall ? 'field crew\u2019s own completion figure, falling back to the installation status' : 'production status of every vendor record on the scope'}.
        Cost paid is what has actually been paid out against that scope; a multi-scope invoice is
        attributed proportionally, since payments are recorded per invoice, not per line.
      </p>
    </div>
  );
}

// Company money with no job behind it — rent, payroll, insurance, utilities.
// Recurring entries are stored ONCE and expanded on read (expandCashEntry),
// so changing the rent changes every future occurrence and rewrites no history.
// Company cards. The value here is timing: a vendor paid by card doesn't take
// cash on the invoice date — it takes cash when the statement is paid, which is
// what the Financial Calendar now models.
// One card charge, added or edited by hand. Same two destinations as the
// import — company overhead, or a job's miscellaneous expense — because a
// charge typed in should behave exactly like the same charge imported. One
// shape, two entry points, no second set of rules.
// Editing is the same modal: a line already on the card arrives as `entry`,
// and can be corrected, recategorised, or moved between company and job. A
// move is a delete-and-recreate because the two live in different places
// (a cash entry vs. a project's AP invoice) — the modal hides that, the
// change log does not.
function CardChargeModal({ open, onClose, ctx, card, entry }) {
  const blank = { date: todayISO(), description: '', amount: '', isCredit: false, dest: 'company', category: '', projectId: '', scopeId: '' };
  const [f, setF] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (entry) setF({
      date: entry.date || todayISO(), description: entry.description || '', amount: String(entry.amount || ''),
      isCredit: !!entry.isCredit, dest: entry.kind === 'job' ? 'job' : 'company',
      category: entry.category || '', projectId: entry.projectId || '',
      scopeId: entry.kind === 'job' ? (entry.scopeId || '__job__') : '',
    });
    else setF(blank);
  }, [open, entry && entry.key]);
  if (!card) return null;
  const editing = !!entry;
  const isJob = f.dest === 'job';
  const cats = isJob ? MISC_EXPENSE_CATEGORIES : (f.isCredit ? CASH_CATEGORIES_IN : CASH_CATEGORIES_OUT);
  const ready = f.description.trim() && Number(f.amount) > 0 && f.category && (!isJob || (f.projectId && f.scopeId));
  const desc = f.description.trim();
  const amount = Number(f.amount) || 0;
  const jobPayload = () => ({
    vendorName: desc, scopeId: f.scopeId === '__job__' ? '' : (f.scopeId || ''),
    expenseCategory: f.category,
    invoiceNumber: `CARD-${(card.last4 || '').trim() || 'X'}-${f.date.replace(/-/g, '')}`,
    invoiceDate: f.date, dueDate: f.date, amount,
    recoverability: 'Pending Determination',
    description: `${desc} — charged to ${card.name}`,
    notes: `Entered on the ${card.name} statement`,
    paidByCardId: card.id,
  });
  const companyPayload = () => ({
    direction: f.isCredit ? 'in' : 'out', label: desc, category: f.category,
    amount, date: f.date, recurrence: 'One-off', projectId: null, creditCardId: card.id,
    notes: `Entered on the ${card.name} statement`,
  });
  function submit() {
    if (!editing) {
      if (isJob) ctx.addMiscInvoice(f.projectId, jobPayload()); else ctx.addCashEntry(companyPayload());
    } else if (entry.kind === 'company' && !isJob) {
      ctx.updateCashEntry(entry.entryId, { direction: f.isCredit ? 'in' : 'out', label: desc, category: f.category, amount, date: f.date });
    } else if (entry.kind === 'job' && isJob && entry.projectId === f.projectId) {
      ctx.updateMiscInvoice(entry.projectId, entry.invId, {
        vendorName: desc, scopeId: f.scopeId === '__job__' ? '' : (f.scopeId || ''),
        expenseCategory: f.category, invoiceDate: f.date, dueDate: f.date, amount,
        description: `${desc} — charged to ${card.name}`,
      });
    } else {
      // Moved to the other destination, or to a different job.
      if (entry.kind === 'company') ctx.removeCashEntry(entry.entryId);
      else ctx.voidApInvoice(entry.projectId, entry.invId, 'Moved to another destination');
      if (isJob) ctx.addMiscInvoice(f.projectId, jobPayload()); else ctx.addCashEntry(companyPayload());
    }
    onClose();
  }
  function drop() {
    if (!confirm(`Remove "${entry.description}" from this statement?`)) return;
    if (entry.kind === 'company') ctx.removeCashEntry(entry.entryId);
    else ctx.voidApInvoice(entry.projectId, entry.invId, 'Removed from the card statement');
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose}
      title={`${editing ? 'Edit charge' : 'Add a charge'} — ${card.name}${card.last4 ? ` ••${card.last4}` : ''}`}
      footer={<>{editing && <Button variant="ghost" onClick={drop}>Remove</Button>}
               <div className="flex-1" />
               <Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!ready}>{editing ? 'Save changes' : 'Add charge'}</Button></>}>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-3 gap-3">
          <Field label="Date"><TextInput type="date" value={f.date} onChange={e => setF({ ...f, date: e.target.value })} /></Field>
          <Field label="Amount"><TextInput type="number" value={f.amount} onChange={e => setF({ ...f, amount: e.target.value })} /></Field>
          <Field label="Type" hint="A refund or payment is a credit">
            <Select value={f.isCredit ? 'credit' : 'charge'} onChange={e => setF({ ...f, isCredit: e.target.value === 'credit', category: '' })} disabled={isJob}>
              <option value="charge">Charge</option>
              <option value="credit">Credit</option>
            </Select>
          </Field>
        </div>
        <Field label="Description" hint="What it was — this is what shows on the statement and the calendar">
          <TextInput value={f.description} onChange={e => setF({ ...f, description: e.target.value })} placeholder="e.g. Coastal Cabinetry Supply — hardware" />
        </Field>
        <Field label="Charge it to" hint="Company overhead, or the cost of a specific job">
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
            {[{ k: 'company', l: '🏢 Company' }, { k: 'job', l: '🏗️ A job' }].map(o => (
              <button key={o.k} type="button" onClick={() => setF({ ...f, dest: o.k, category: '', projectId: '', scopeId: '' })}
                className={`px-3 py-1.5 rounded-md text-sm font-semibold ${f.dest === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
            ))}
          </div>
        </Field>
        {isJob && (
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Job">
              <Select value={f.projectId} onChange={e => setF({ ...f, projectId: e.target.value, scopeId: '' })}>
                <option value="">— choose a job —</option>
                {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              </Select>
            </Field>
            <Field label="Scope" hint="Puts the cost in that scope's actual cost">
              <Select value={f.scopeId} onChange={e => setF({ ...f, scopeId: e.target.value })} disabled={!f.projectId}>
                <option value="">— choose a scope —</option>
                {(ctx.projects.find(p => p.id === f.projectId) || { scopes: [] }).scopes.map(sc => <option key={sc.id} value={sc.id}>{sc.name}</option>)}
                <option value="__job__">Not scope-specific</option>
              </Select>
            </Field>
          </div>
        )}
        <Field label="Category">
          <Select value={f.category} onChange={e => setF({ ...f, category: e.target.value })}>
            <option value="">— choose a category —</option>
            {cats.map(c => <option key={c}>{c}</option>)}
          </Select>
        </Field>
        <p className="text-[11px] text-[var(--leon-black)]/45">
          {isJob
            ? 'This becomes a Miscellaneous expense on that job, against the scope you pick, so it lands in that scope’s actual cost.'
            : 'This becomes a company entry on this card, so the cash leaves on the card’s statement due date rather than today.'}
          {editing && ' Changing the amount moves the money too — the old figure comes off before the new one goes on.'}
        </p>
      </div>
    </Modal>
  );
}

// Enter a whole statement at once — pasted, uploaded, or typed by hand. All
// three end at the SAME review table and the same commit, so a statement keyed
// in behaves exactly like one imported. Nothing is created until the table is
// confirmed: rows can be edited, categorised, assigned to a job, or dropped.
function StatementModal({ open, onClose, ctx, card }) {
  const [raw, setRaw] = useState('');
  const [rows, setRows] = useState([]);
  const [step, setStep] = useState('start');   // start -> review
  const [mode, setMode] = useState('paste');   // paste | type
  const fileRef = useRef(null);
  useEffect(() => { if (open) { setRaw(''); setRows([]); setStep('start'); setMode('paste'); } }, [open]);
  if (!card) return null;

  // No default category. A bank line says nothing about which bucket it belongs
  // in, and a default is just a wrong answer that nobody notices — so each line
  // is categorised deliberately and an uncategorised line cannot be imported.
  function parse(text) { setRows(parseStatementCsv(text)); setStep('review'); }
  function blankRow() {
    return { id: uid('stmt'), include: true, date: todayISO(), description: '', amount: 0, isCredit: false, dest: 'company', category: null, projectId: null, scopeId: null };
  }
  function startBlank(n) { setRows(Array.from({ length: n }, blankRow)); setStep('review'); }
  async function onFile(e) {
    const f = e.target.files[0];
    if (!f) return;
    const text = await f.text();
    setRaw(text); parse(text); e.target.value = '';
  }
  function setRow(id, fields) { setRows(prev => prev.map(r => r.id === id ? { ...r, ...fields } : r)); }
  const chosen = rows.filter(r => r.include);
  // A line is either company overhead or a cost against a job. The second
  // becomes a Miscellaneous invoice on that project so it lands in the scope's
  // actual cost — a card charge for a job is job cost, not overhead.
  // A job line has to say WHICH SCOPE it belongs to. Defaulting silently to
  // "whole job" meant the cost never reached a scope's actual cost — the same
  // silent-default problem the category picker already avoids. '__job__' is the
  // explicit "not scope-specific" answer, which is different from unanswered.
  const blankLines = chosen.filter(r => !r.description.trim() || !r.date || !(Number(r.amount) > 0));
  const uncategorised = chosen.filter(r => (r.dest === 'job' ? (!r.projectId || !r.scopeId) : !r.category));
  const incomplete = chosen.filter(r => blankLines.includes(r) || uncategorised.includes(r));
  const total = chosen.reduce((n, r) => n + (r.isCredit ? -Number(r.amount) : Number(r.amount)), 0);
  // Which statement these lines actually land on, per the card's own cycle.
  const bills = Array.from(new Set(chosen.filter(r => r.date).map(r => cardStatementFor(card, r.date).dueDate))).sort();

  function commit() {
    chosen.forEach(r => {
      const amount = Number(r.amount) || 0;
      if (r.dest === 'job' && r.projectId) {
        ctx.addMiscInvoice(r.projectId, {
          vendorName: r.description.trim(),
          scopeId: r.scopeId === '__job__' ? '' : (r.scopeId || ''),
          expenseCategory: r.category || MISC_EXPENSE_CATEGORIES[0],
          invoiceNumber: `CARD-${(card.last4 || '').trim() || 'X'}-${r.date.replace(/-/g, '')}`,
          invoiceDate: r.date, dueDate: r.date, amount,
          recoverability: 'Pending Determination',
          description: `${r.description.trim()} — charged to ${card.name}`,
          notes: `Entered on the ${card.name} statement`,
          paidByCardId: card.id,
        });
        return;
      }
      ctx.addCashEntry({
        direction: r.isCredit ? 'in' : 'out', label: r.description.trim(),
        category: r.category, amount, date: r.date, recurrence: 'One-off',
        projectId: null, creditCardId: card.id,
        notes: `Entered on the ${card.name} statement`,
      });
    });
    onClose();
  }

  return (
    <Modal wide open={open} onClose={onClose}
      title={`Enter a statement — ${card.name}${card.last4 ? ` ••${card.last4}` : ''}`}
      footer={step === 'review'
        ? <><Button variant="ghost" onClick={() => setStep('start')}>Back</Button>
            <div className="flex-1" />
            <Button variant="outline" onClick={() => setRows(prev => [...prev, blankRow()])}>+ Add line</Button>
            <Button onClick={commit} disabled={!chosen.length || incomplete.length > 0}>
              {incomplete.length > 0
                ? `${incomplete.length} line${incomplete.length === 1 ? '' : 's'} still incomplete`
                : `Save ${chosen.length} entr${chosen.length === 1 ? 'y' : 'ies'}`}
            </Button></>
        : <><Button variant="ghost" onClick={onClose}>Cancel</Button>
            {mode === 'paste'
              ? <Button onClick={() => parse(raw)} disabled={!raw.trim()}>Read statement</Button>
              : <Button onClick={() => startBlank(5)}>Start typing</Button>}</>}>
      {step === 'start' ? (
        <div className="space-y-3">
          <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
            {[{ k: 'paste', l: '⬆ Paste or upload' }, { k: 'type', l: '⌨️ Type it in' }].map(o => (
              <button key={o.k} type="button" onClick={() => setMode(o.k)}
                className={`px-3 py-1.5 rounded-md text-sm font-semibold ${mode === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
            ))}
          </div>
          {mode === 'paste' ? (
            <>
              <p className="text-sm text-[var(--leon-black)]/55">
                Upload the CSV your bank exports, or paste the rows straight from a spreadsheet.
                Columns are worked out from the content &mdash; a date, an amount, and whatever text
                is left becomes the description &mdash; so the column order does not matter.
              </p>
              <div className="flex items-center gap-2">
                <Button variant="outline" onClick={() => fileRef.current.click()}>&#11014; Upload CSV</Button>
                <input ref={fileRef} type="file" accept=".csv,text/csv,text/plain" className="hidden" onChange={onFile} />
                <span className="text-xs text-[var(--leon-black)]/40">or paste below</span>
              </div>
              <TextArea rows={8} value={raw} onChange={e => setRaw(e.target.value)}
                placeholder={'08/14/2026,COASTAL CABINETRY SUPPLY,1240.55\n08/16/2026,SHELL OIL 4471,86.20\n08/19/2026,PAYMENT THANK YOU,(2000.00)'} />
            </>
          ) : (
            <>
              <p className="text-sm text-[var(--leon-black)]/55">
                Key the statement in line by line &mdash; for a paper statement, or a card with no
                export. You get the same table the import fills in, with blank rows to start:
                date, description, amount, then where it belongs. Add or drop lines as you go.
              </p>
              <div className="flex items-center gap-2 flex-wrap">
                <Button variant="outline" onClick={() => startBlank(5)}>Start with 5 lines</Button>
                <Button variant="ghost" onClick={() => startBlank(12)}>Start with 12</Button>
                <span className="text-xs text-[var(--leon-black)]/40">you can add more at any point</span>
              </div>
            </>
          )}
          <p className="text-xs text-[var(--leon-black)]/50">
            You will categorise each line individually in the next step &mdash; there is no default,
            because a bank line does not tell you which bucket it belongs in.
          </p>
        </div>
      ) : (
        <div className="space-y-3">
          {rows.length === 0 ? (
            <EmptyState text="Nothing readable in that — every row needs a date and an amount." />
          ) : (
            <>
              <div className="flex items-center gap-2 flex-wrap text-xs">
                <b>{chosen.length} of {rows.length}</b> selected
                <button onClick={() => setRows(prev => prev.map(r => ({ ...r, include: true })))} className="font-semibold text-[var(--leon-brown)]">All</button>
                <button onClick={() => setRows(prev => prev.map(r => ({ ...r, include: false })))} className="font-semibold text-[var(--leon-black)]/45">None</button>
                {incomplete.length > 0 && (
                  <span className="text-[var(--leon-yellow)] font-semibold">{incomplete.length} incomplete</span>
                )}
                {bills.length > 0 && (
                  <span className="text-[var(--leon-black)]/45">
                    lands on the {bills.map(b => fmtDate(b)).join(' and ')} bill{bills.length > 1 ? 's' : ''}
                  </span>
                )}
                <div className="flex-1" />
                <span className="tabular-nums">Net <b>{fmtMoney(total)}</b></span>
              </div>
              <div className="border border-[var(--leon-line)] rounded-xl overflow-x-auto max-h-96 overflow-y-auto">
                {/* Description is the column that actually needs the room — the
                    others are fixed-width controls. */}
                <table className="w-full text-xs" style={{ minWidth: 980 }}>
                  <thead className="sticky top-0 bg-[var(--leon-cream)]">
                    <tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50">
                      <th className="px-2 py-2 w-8"></th>
                      <th className="px-2 py-2" style={{ width: 132 }}>Date</th>
                      <th className="px-2 py-2" style={{ minWidth: 220 }}>Description</th>
                      <th className="px-2 py-2" style={{ width: 150 }}>Charge to</th>
                      <th className="px-2 py-2" style={{ width: 190 }}>Category</th>
                      <th className="px-2 py-2" style={{ width: 200 }}>Job / scope</th>
                      <th className="px-2 py-2 text-right" style={{ width: 160 }}>Amount</th>
                      <th className="px-2 py-2 w-8"></th>
                    </tr>
                  </thead>
                  <tbody>
                    {rows.map(r => (
                      <tr key={r.id} className={`border-t border-[var(--leon-line)] ${r.include ? '' : 'opacity-40'}`}>
                        <td className="px-2 py-1.5">
                          <input type="checkbox" checked={r.include} onChange={e => setRow(r.id, { include: e.target.checked })} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
                        </td>
                        <td className="px-2 py-1.5"><TextInput type="date" value={r.date} onChange={e => setRow(r.id, { date: e.target.value })} className={`!w-[7.5rem] !py-0.5 !text-xs ${r.include && !r.date ? '!border-[var(--leon-yellow)]' : ''}`} /></td>
                        <td className="px-2 py-1.5"><TextInput value={r.description} onChange={e => setRow(r.id, { description: e.target.value })} placeholder="What it was" className={`w-full !py-0.5 !text-xs ${r.include && !r.description.trim() ? '!border-[var(--leon-yellow)]' : ''}`} /></td>
                        <td className="px-2 py-1.5">
                          <Select value={r.dest || 'company'} onChange={e => setRow(r.id, { dest: e.target.value, category: null, projectId: null, scopeId: null })} className="!w-[8.5rem] !py-0.5 !text-xs">
                            <option value="company">Company</option>
                            <option value="job">A job</option>
                          </Select>
                        </td>
                        <td className="px-2 py-1.5">
                          <Select value={r.category || ''} onChange={e => setRow(r.id, { category: e.target.value || null })}
                            className={`!w-44 !py-0.5 !text-xs ${r.include && !r.category ? '!border-[var(--leon-yellow)]' : ''}`}>
                            <option value="">— choose a category —</option>
                            {(r.dest === 'job' ? MISC_EXPENSE_CATEGORIES : (r.isCredit ? CASH_CATEGORIES_IN : CASH_CATEGORIES_OUT)).map(c => <option key={c}>{c}</option>)}
                          </Select>
                        </td>
                        <td className="px-2 py-1.5">
                          {r.dest === 'job' ? (
                            <div className="flex flex-col gap-1">
                              <Select value={r.projectId || ''} onChange={e => setRow(r.id, { projectId: e.target.value || null, scopeId: null })}
                                className={`!w-[11.5rem] !py-0.5 !text-xs ${r.include && !r.projectId ? '!border-[var(--leon-yellow)]' : ''}`}>
                                <option value="">— choose a job —</option>
                                {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                              </Select>
                              {r.projectId && (
                                <Select value={r.scopeId || ''} onChange={e => setRow(r.id, { scopeId: e.target.value || null })}
                                  className={`!w-[11.5rem] !py-0.5 !text-xs ${r.include && !r.scopeId ? '!border-[var(--leon-yellow)]' : ''}`}>
                                  <option value="">— choose a scope —</option>
                                  {(ctx.projects.find(p => p.id === r.projectId) || { scopes: [] }).scopes.map(sc => <option key={sc.id} value={sc.id}>{sc.name}</option>)}
                                  <option value="__job__">Not scope-specific</option>
                                </Select>
                              )}
                            </div>
                          ) : <span className="text-[var(--leon-black)]/30">—</span>}
                        </td>
                        <td className="px-2 py-1.5">
                          <div className="flex items-center gap-1 justify-end">
                            <TextInput type="number" value={r.amount} onChange={e => setRow(r.id, { amount: e.target.value })}
                              className={`!w-24 !py-0.5 !text-xs text-right ${r.include && !(Number(r.amount) > 0) ? '!border-[var(--leon-yellow)]' : ''}`} />
                            <label className="flex items-center gap-1 text-[10px] text-[var(--leon-black)]/50 cursor-pointer" title="A refund or payment is a credit">
                              <input type="checkbox" checked={!!r.isCredit} onChange={e => setRow(r.id, { isCredit: e.target.checked, category: null })} className="w-3 h-3 accent-[var(--leon-brown)]" />cr
                            </label>
                          </div>
                        </td>
                        <td className="px-2 py-1.5">
                          <IconBtn title="Drop this row" onClick={() => setRows(prev => prev.filter(x => x.id !== r.id))}>&#10005;</IconBtn>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <p className="text-[11px] text-[var(--leon-black)]/45">
                Every line is categorised individually &mdash; nothing is guessed. <b>Company</b> lines
                become entries on this card, so the calendar shows the cash leaving on the
                card&rsquo;s statement due date rather than the purchase date. <b>A job</b> creates a
                Miscellaneous expense on that project instead, against the scope you pick, so it
                lands in that scope&rsquo;s actual cost. Choose <b>Not scope-specific</b> only when it
                genuinely belongs to the whole job &mdash; that is a decision, not a default. Tick
                <b> cr</b> for a refund or a payment to the card: it comes off the bill rather than
                adding to it. Every line stays editable afterwards, from the card itself.
              </p>
            </>
          )}
        </div>
      )}
    </Modal>
  );
}

function CreditCardsView({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [editing, setEditing] = useState(null);
  const [stmtFor, setStmtFor] = useState(null);
  const [chargeFor, setChargeFor] = useState(null);      // card, for a NEW charge
  const [editCharge, setEditCharge] = useState(null);    // { card, entry }, for an existing one
  const [q, setQ] = useState('');
  const [fDest, setFDest] = useState('all');
  const [fCat, setFCat] = useState('all');
  const [from, setFrom] = useState('');
  const [to, setTo] = useState('');
  const cards = (ctx.creditCards || []).filter(c => c.active !== false);

  // Every charge currently sitting on each card, from both sources, carrying
  // enough of its origin to be opened and corrected in place.
  const charges = useMemo(() => {
    const out = {};
    cards.forEach(c => { out[c.id] = []; });
    ctx.projects.forEach(p => liveApInvoices(p).forEach(inv => (inv.payments || []).forEach(pay => {
      if (pay.creditCardId && out[pay.creditCardId]) {
        const sc = (p.scopes || []).find(x => x.id === inv.scopeId);
        out[pay.creditCardId].push({
          key: `job:${inv.id}`, kind: 'job', date: pay.date, amount: Number(pay.amount) || 0, isCredit: false,
          description: inv.vendorName || inv.invoiceNumber, category: inv.expenseCategory || '',
          detail: `${p.name}${sc ? ` · ${sc.name}` : ' · whole job'} · ${inv.invoiceNumber}`,
          projectId: p.id, invId: inv.id, scopeId: inv.scopeId || '',
        });
      }
    })));
    (ctx.cashEntries || []).filter(e => e.active !== false && e.creditCardId).forEach(e => {
      if (out[e.creditCardId]) out[e.creditCardId].push({
        key: `co:${e.id}`, kind: 'company', date: e.date, amount: Number(e.amount) || 0,
        isCredit: e.direction === 'in', description: e.label, category: e.category || '',
        detail: `Company${e.recurrence && e.recurrence !== 'One-off' ? ` · ${e.recurrence}` : ''}`,
        entryId: e.id,
      });
    });
    Object.values(out).forEach(list => list.sort((a, b) => (a.date < b.date ? 1 : -1)));
    return out;
  }, [ctx.projects, ctx.cashEntries, cards]);

  const allCats = useMemo(() => {
    const s = new Set();
    Object.values(charges).forEach(list => list.forEach(x => { if (x.category) s.add(x.category); }));
    return [...s].sort();
  }, [charges]);

  const ql = q.trim().toLowerCase();
  const filterOn = !!(ql || fDest !== 'all' || fCat !== 'all' || from || to);
  function keep(x) {
    if (ql && !`${x.description} ${x.detail} ${x.category}`.toLowerCase().includes(ql)) return false;
    if (fDest !== 'all' && x.kind !== fDest) return false;
    if (fCat !== 'all' && x.category !== fCat) return false;
    if (from && x.date < from) return false;
    if (to && x.date > to) return false;
    return true;
  }
  // Charges are grouped into the statement they actually fall on — the card's
  // own cycle decides that, not the calendar month, so a charge after the
  // closing day belongs to next month's bill.
  function statementsFor(card, list) {
    const by = {};
    list.forEach(x => {
      const { closeDate, dueDate } = cardStatementFor(card, x.date);
      if (!by[dueDate]) by[dueDate] = { dueDate, closeDate, ...cardStatementRange(card, closeDate), items: [], total: 0 };
      by[dueDate].items.push(x);
      by[dueDate].total += x.isCredit ? -x.amount : x.amount;
    });
    return Object.values(by).sort((a, b) => (a.dueDate < b.dueDate ? 1 : -1));
  }

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        Charges routed to a card don&rsquo;t leave cash on their own date &mdash; they roll into that
        card&rsquo;s statement, and the Financial Calendar shows the bill as one payment on its due date.
        Job costs and company overhead can both be charged here, and every line stays editable.
      </p>
      {cards.length > 0 && (
        <div className="flex flex-wrap items-center gap-2 mb-3">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search charges…" className="!w-56" />
          <Select value={fDest} onChange={e => setFDest(e.target.value)} className="!w-40">
            <option value="all">All charges</option>
            <option value="company">Company only</option>
            <option value="job">Job costs only</option>
          </Select>
          <Select value={fCat} onChange={e => setFCat(e.target.value)} className="!w-48">
            <option value="all">All categories</option>
            {allCats.map(c => <option key={c}>{c}</option>)}
          </Select>
          <div className="flex items-center gap-1 text-xs text-[var(--leon-black)]/45">
            <TextInput type="date" value={from} onChange={e => setFrom(e.target.value)} className="!w-36" title="From" />
            <span>to</span>
            <TextInput type="date" value={to} onChange={e => setTo(e.target.value)} className="!w-36" title="To" />
          </div>
          {filterOn && <Button size="sm" variant="ghost" onClick={() => { setQ(''); setFDest('all'); setFCat('all'); setFrom(''); setTo(''); }}>Clear</Button>}
          <div className="flex-1" />
          <Button size="sm" onClick={() => { setEditing(null); setShowAdd(true); }}>+ Add Card</Button>
        </div>
      )}
      {cards.length === 0 ? <EmptyState text="No company cards yet. Add one to start tracking card charges and their monthly bill." /> : (
        <div className="space-y-3">
          {cards.map(c => {
            const all = charges[c.id] || [];
            const list = all.filter(keep);
            // The balance is what the card owes across everything on it; the
            // filtered figure is what you are looking at. Showing only one of
            // them would make a filter look like a change in the balance.
            const balance = all.reduce((n, x) => n + (x.isCredit ? -x.amount : x.amount), 0);
            const shown = list.reduce((n, x) => n + (x.isCredit ? -x.amount : x.amount), 0);
            const next = cardStatementFor(c, todayISO());
            const groups = statementsFor(c, list);
            return (
              <div key={c.id} className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
                <div className="flex items-center gap-3 px-3 py-2 bg-[var(--leon-cream)] flex-wrap">
                  <span className="text-sm font-bold">💳 {c.name}{c.last4 ? ` ••${c.last4}` : ''}</span>
                  {c.issuer && <span className="text-xs text-[var(--leon-black)]/45">{c.issuer}</span>}
                  <span className="text-[11px] text-[var(--leon-black)]/45">
                    Statement closes day {c.statementDay} · due day {c.dueDay} · next bill {fmtDate(next.dueDate)}
                  </span>
                  <div className="flex-1" />
                  <span className="text-sm font-bold tabular-nums" style={{ color: scheduleColor('Delayed') }}>{fmtMoney(balance)}</span>
                  <Button size="sm" variant="ghost" onClick={() => setChargeFor(c)}>+ Add Charge</Button>
                  <Button size="sm" variant="ghost" onClick={() => setStmtFor(c)}>&#11014; Enter Statement</Button>
                  <Button size="sm" variant="ghost" onClick={() => { setEditing(c); setShowAdd(true); }}>Edit</Button>
                  <IconBtn title="Remove" onClick={() => { if (confirm(`Remove ${c.name}?`)) ctx.removeCreditCard(c.id); }}>✕</IconBtn>
                </div>
                {c.creditLimit > 0 && (
                  <div className="px-3 pt-2">
                    <div className="h-1.5 rounded-full bg-[var(--leon-cream)] overflow-hidden">
                      <div className="h-full rounded-full" style={{ width: `${Math.min(100, Math.max(0, (balance / c.creditLimit) * 100))}%`, background: scheduleGradient(balance / c.creditLimit > 0.8 ? 'Delayed' : 'In Progress') }} />
                    </div>
                    <p className="text-[10px] text-[var(--leon-black)]/40 mt-0.5">{fmtMoney(balance)} of {fmtMoney(c.creditLimit)} limit</p>
                  </div>
                )}
                {filterOn && (
                  <p className="px-3 pt-2 text-[11px] text-[var(--leon-black)]/50">
                    Showing <b>{list.length}</b> of {all.length} charge{all.length === 1 ? '' : 's'} · <b className="tabular-nums">{fmtMoney(shown)}</b> filtered
                  </p>
                )}
                {list.length === 0 ? (
                  <p className="px-3 py-2 text-xs text-[var(--leon-black)]/40 italic">
                    {all.length ? 'No charges match those filters.' : 'No charges on this card yet.'}
                  </p>
                ) : (
                  <div className="p-2 space-y-2">
                    {groups.map((g, gi) => (
                      <Collapsible key={g.dueDate} id={`card-${c.id}-${g.dueDate}`} defaultOpen={gi === 0}
                        right={<span className="tabular-nums font-bold text-sm">{fmtMoney(g.total)}</span>}
                        title={
                          <span className="flex items-center gap-2 flex-wrap">
                            <b>{fmtDate(g.dueDate)} bill</b>
                            <span className="text-[11px] font-medium text-[var(--leon-black)]/45">
                              {fmtDate(g.startDate)} – {fmtDate(g.closeDate)} · {g.items.length} line{g.items.length === 1 ? '' : 's'}
                            </span>
                            {g.dueDate === next.dueDate && <Badge tone="yellow">current</Badge>}
                          </span>
                        }>
                        <div className="divide-y divide-[var(--leon-line)]">
                          {g.items.map(x => (
                            <div key={x.key} className="flex items-center gap-3 px-1 py-1.5 text-xs group">
                              <span className="text-[var(--leon-black)]/40 w-20 shrink-0">{fmtDate(x.date)}</span>
                              <span className="flex-1 min-w-0 truncate">
                                <b>{x.description}</b>{' '}
                                <span className="text-[var(--leon-black)]/45">{x.detail}</span>
                                {x.category && <span className="text-[var(--leon-black)]/35"> · {x.category}</span>}
                              </span>
                              {x.kind === 'job' && <Badge tone="brown">job</Badge>}
                              {x.isCredit && <Badge tone="green">credit</Badge>}
                              <span className="font-semibold tabular-nums shrink-0">{x.isCredit ? '−' : ''}{fmtMoney(x.amount)}</span>
                              <IconBtn title="Edit this charge" onClick={() => setEditCharge({ card: c, entry: x })}>&#9998;</IconBtn>
                            </div>
                          ))}
                        </div>
                      </Collapsible>
                    ))}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
      <CreditCardModal open={showAdd} card={editing} onClose={() => { setShowAdd(false); setEditing(null); }} ctx={ctx} />
      <StatementModal open={!!stmtFor} card={stmtFor} onClose={() => setStmtFor(null)} ctx={ctx} />
      <CardChargeModal open={!!chargeFor} card={chargeFor} entry={null} onClose={() => setChargeFor(null)} ctx={ctx} />
      <CardChargeModal open={!!editCharge} card={editCharge && editCharge.card} entry={editCharge && editCharge.entry}
        onClose={() => setEditCharge(null)} ctx={ctx} />
    </div>
  );
}

function CreditCardModal({ open, card, onClose, ctx }) {
  const blank = { name: '', issuer: '', last4: '', statementDay: 25, dueDay: 15, creditLimit: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => { if (open) setForm(card ? { ...blank, ...card, creditLimit: String(card.creditLimit || '') } : blank); }, [open, card && card.id]);
  function submit() {
    if (!form.name.trim()) return;
    const payload = { ...form, creditLimit: Number(form.creditLimit) || 0 };
    if (card) ctx.updateCreditCard(card.id, payload); else ctx.addCreditCard(payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} title={card ? `Edit — ${card.name}` : 'Add Credit Card'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{card ? 'Save' : 'Add Card'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Card name"><TextInput autoFocus value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="e.g. Company Amex" /></Field>
          <Field label="Issuer"><TextInput value={form.issuer} onChange={e => setForm({ ...form, issuer: e.target.value })} placeholder="Amex, Chase…" /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Last 4 digits"><TextInput value={form.last4} maxLength={4} onChange={e => setForm({ ...form, last4: e.target.value.replace(/\D/g, '') })} /></Field>
          <Field label="Statement closes (day)" hint="Charges after this roll to next month"><TextInput type="number" min="1" max="28" value={form.statementDay} onChange={e => setForm({ ...form, statementDay: Number(e.target.value) || 25 })} /></Field>
          <Field label="Payment due (day)"><TextInput type="number" min="1" max="28" value={form.dueDay} onChange={e => setForm({ ...form, dueDay: Number(e.target.value) || 15 })} /></Field>
        </div>
        <Field label="Credit limit (optional)"><TextInput type="number" min="0" value={form.creditLimit} onChange={e => setForm({ ...form, creditLimit: e.target.value })} /></Field>
        <Field label="Notes"><TextInput value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
        <p className="text-[11px] text-[var(--leon-black)]/40">Only the last 4 digits are stored &mdash; never a full card number.</p>
      </div>
    </Modal>
  );
}

function CashEntriesView({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const [editing, setEditing] = useState(null);
  const [dir, setDir] = useState('all');
  const live = (ctx.cashEntries || []).filter(e => e.active !== false);
  const shown = live.filter(e => dir === 'all' || e.direction === dir);
  const monthlyOut = live.filter(e => e.direction === 'out').reduce((n, e) => n + monthlyEquivalent(e), 0);
  const monthlyIn = live.filter(e => e.direction === 'in').reduce((n, e) => n + monthlyEquivalent(e), 0);

  return (
    <div>
      <div className="grid sm:grid-cols-3 gap-3 mb-4">
        <StatBox label="Recurring In / month" value={fmtMoney(monthlyIn)} />
        <StatBox label="Recurring Out / month" value={fmtMoney(monthlyOut)} />
        <StatBox label="Net / month" value={fmtMoney(monthlyIn - monthlyOut)} tone={monthlyIn - monthlyOut < 0 ? 'red' : undefined} />
      </div>

      <div className="flex items-center gap-2 mb-3 flex-wrap">
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {[{ k: 'all', l: 'All' }, { k: 'in', l: 'Income' }, { k: 'out', l: 'Expenses' }].map(o => (
            <button key={o.k} onClick={() => setDir(o.k)}
              className={`px-3 py-1 rounded-md text-xs font-semibold ${dir === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
          ))}
        </div>
        <div className="flex-1" />
        <Button size="sm" onClick={() => { setEditing(null); setShowAdd(true); }}>+ Add Entry</Button>
      </div>

      {shown.length === 0 ? <EmptyState text="No company income or expenses entered yet — add rent, payroll, insurance and utilities so the calendar reflects real cash." /> : (
        <div className="bg-white border border-[var(--leon-line)] rounded-xl overflow-hidden overflow-x-auto">
          <table className="w-full text-sm">
            <thead className="bg-[var(--leon-cream)]"><tr className="text-left text-xs uppercase text-[var(--leon-black)]/50">
              <th className="px-3 py-2">Entry</th><th className="px-3 py-2">Category</th><th className="px-3 py-2">Starts</th>
              <th className="px-3 py-2">Repeats</th><th className="px-3 py-2 text-right">Amount</th><th className="px-3 py-2"></th>
            </tr></thead>
            <tbody>
              {shown.map(e => (
                <tr key={e.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-3 py-2">
                    <span className="font-semibold">{e.label}</span>
                    {e.projectId && <span className="ml-2 text-[10px] text-[var(--leon-black)]/40">{(ctx.projects.find(p => p.id === e.projectId) || {}).name}</span>}
                    {e.notes && <p className="text-[11px] text-[var(--leon-black)]/45">{e.notes}</p>}
                  </td>
                  <td className="px-3 py-2 text-xs">{e.category}</td>
                  <td className="px-3 py-2 text-xs">{fmtDate(e.date)}{e.endDate ? ` – ${fmtDate(e.endDate)}` : ''}</td>
                  <td className="px-3 py-2 text-xs">{e.recurrence}</td>
                  <td className="px-3 py-2 text-right font-bold tabular-nums"
                      style={{ color: e.direction === 'in' ? scheduleColor('Complete') : scheduleColor('Delayed') }}>
                    {e.direction === 'in' ? '+' : '−'}{fmtMoney(e.amount)}
                  </td>
                  <td className="px-3 py-2 text-right whitespace-nowrap">
                    <Button size="sm" variant="ghost" onClick={() => { setEditing(e); setShowAdd(true); }}>Edit</Button>
                    <IconBtn title="Remove" onClick={() => { if (confirm(`Remove "${e.label}"?`)) ctx.removeCashEntry(e.id); }}>✕</IconBtn>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <CashEntryModal open={showAdd} entry={editing} onClose={() => { setShowAdd(false); setEditing(null); }} ctx={ctx} />
    </div>
  );
}
// Normalises any recurrence to a per-month figure, so "annual insurance" and
// "monthly rent" can be summed into one meaningful number.
function monthlyEquivalent(e) {
  const a = Number(e.amount) || 0;
  switch (e.recurrence) {
    case 'Weekly': return a * 52 / 12;
    case 'Monthly': return a;
    case 'Quarterly': return a / 3;
    case 'Annually': return a / 12;
    default: return 0;   // one-off isn't a recurring commitment
  }
}

function CashEntryModal({ open, entry, onClose, ctx }) {
  const blank = { direction: 'out', label: '', category: 'Rent', amount: '', date: todayISO(), recurrence: 'Monthly', endDate: '', projectId: '', creditCardId: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    setForm(entry ? { ...blank, ...entry, amount: String(entry.amount), endDate: entry.endDate || '', projectId: entry.projectId || '' } : blank);
  }, [open, entry && entry.id]);
  const cats = form.direction === 'in' ? CASH_CATEGORIES_IN : CASH_CATEGORIES_OUT;
  function submit() {
    if (!form.label.trim() || !(Number(form.amount) > 0)) return;
    const payload = { ...form, amount: Number(form.amount), endDate: form.endDate || null, projectId: form.projectId || null, creditCardId: form.creditCardId || null };
    if (entry) ctx.updateCashEntry(entry.id, payload); else ctx.addCashEntry(payload);
    onClose();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={entry ? `Edit — ${entry.label}` : 'Add Income or Expense'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit}>{entry ? 'Save' : 'Add Entry'}</Button></>}>
      <div className="space-y-3">
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
          {[{ k: 'out', l: 'Expense' }, { k: 'in', l: 'Income' }].map(o => (
            <button key={o.k} type="button"
              onClick={() => setForm({ ...form, direction: o.k, category: (o.k === 'in' ? CASH_CATEGORIES_IN : CASH_CATEGORIES_OUT)[0] })}
              className={`px-4 py-1.5 rounded-md text-xs font-semibold ${form.direction === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
          ))}
        </div>
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Description"><TextInput autoFocus value={form.label} onChange={e => setForm({ ...form, label: e.target.value })} placeholder="e.g. Warehouse rent" /></Field>
          <Field label="Category"><Select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>{cats.map(c => <option key={c}>{c}</option>)}</Select></Field>
        </div>
        <div className="grid sm:grid-cols-3 gap-3">
          <Field label="Amount"><TextInput type="number" min="0" value={form.amount} onChange={e => setForm({ ...form, amount: e.target.value })} /></Field>
          <Field label="First date"><TextInput type="date" value={form.date} onChange={e => setForm({ ...form, date: e.target.value })} /></Field>
          <Field label="Repeats"><Select value={form.recurrence} onChange={e => setForm({ ...form, recurrence: e.target.value })}>{CASH_RECURRENCE.map(r => <option key={r}>{r}</option>)}</Select></Field>
        </div>
        {form.recurrence !== 'One-off' && (
          <Field label="Stop after (optional)" hint="Leave blank if it continues indefinitely."><TextInput type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} /></Field>
        )}
        {form.direction === 'out' && (ctx.creditCards || []).filter(c => c.active !== false).length > 0 && (
          <Field label="Paid by" hint="On a card, the cash leaves when that card's bill is paid — not on the date above.">
            <Select value={form.creditCardId || ''} onChange={e => setForm({ ...form, creditCardId: e.target.value })}>
              <option value="">Cash / bank</option>
              {(ctx.creditCards || []).filter(c => c.active !== false).map(c => <option key={c.id} value={c.id}>{c.name}{c.last4 ? ` ••${c.last4}` : ''}</option>)}
            </Select>
          </Field>
        )}
        <Field label="Project (optional)" hint="Only if this cost genuinely belongs to one job — most overhead does not.">
          <Select value={form.projectId} onChange={e => setForm({ ...form, projectId: e.target.value })}>
            <option value="">— company-wide —</option>
            {ctx.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Notes"><TextInput value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function FinancialCalendarView({ ctx, project }) {
  // Opens on the by-day week — the accountant's default question is "what
  // moves this week", not "how does the quarter look".
  const [weeks, setWeeks] = useState(1);
  const [anchorDate, setAnchorDate] = useState(todayISO());
  const [openWeek, setOpenWeek] = useState(null);
  const [showKind, setShowKind] = useState('all');   // all | in | out

  const from = startOfWeekISO(anchorDate);
  const to = addDays(from, weeks * 7 - 1);
  // A per-project view passes one project; the hub passes them all. Accounting
  // deliberately sees every department — they cover both sets of books.
  const projects = project ? [project] : ctx.projects;
  const events = useMemo(
    () => buildCashEvents(projects, project ? [] : ctx.cashEntries, from, to, ctx.creditCards),
    [projects, ctx.cashEntries, ctx.creditCards, from, to, project]);
  // A per-project view has no company bank balance, so it stays a pure net.
  const settings = project ? null : ctx.cashSettings;
  const dayMode = weeks === 1;
  // Changing the range or the unit invalidates which column was open.
  useEffect(() => { setOpenWeek(null); }, [weeks, anchorDate]);
  const buckets = useMemo(() => bucketCashByWeek(events, from, weeks, settings, dayMode ? 'day' : 'week'), [events, from, weeks, settings, dayMode]);

  const totalIn = buckets.reduce((n, b) => n + b.in, 0);
  const totalOut = buckets.reduce((n, b) => n + b.out, 0);
  const maxBar = Math.max(1, ...buckets.map(b => Math.max(b.in, b.out)));
  const today = todayISO();

  return (
    <div>
      <div className="flex items-center gap-2 flex-wrap mb-4">
        <Button size="sm" variant="ghost" onClick={() => setAnchorDate(addDays(from, -7 * Math.max(1, weeks)))}>&larr; Earlier</Button>
        <Button size="sm" variant="ghost" onClick={() => setAnchorDate(todayISO())}>Today</Button>
        <Button size="sm" variant="ghost" onClick={() => setAnchorDate(addDays(from, 7 * Math.max(1, weeks)))}>Later &rarr;</Button>
        <Select value={weeks} onChange={e => setWeeks(Number(e.target.value))} className="!w-36 !py-1 !text-xs">
          <option value={1}>1 week (by day)</option>
          {[2, 4, 6, 8, 10, 26].map(w => <option key={w} value={w}>{w} weeks</option>)}
        </Select>
        <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
          {[{ k: 'all', l: 'Both' }, { k: 'in', l: 'Money In' }, { k: 'out', l: 'Money Out' }].map(o => (
            <button key={o.k} onClick={() => setShowKind(o.k)}
              className={`px-2.5 py-1 rounded-md text-xs font-semibold ${showKind === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
          ))}
        </div>
        <div className="flex-1" />
        <div className="flex gap-3 text-xs">
          <span><span className="text-[var(--leon-black)]/45">In </span><b style={{ color: scheduleColor('Complete') }}>{fmtMoney(totalIn)}</b></span>
          <span><span className="text-[var(--leon-black)]/45">Out </span><b style={{ color: scheduleColor('Delayed') }}>{fmtMoney(totalOut)}</b></span>
          <span><span className="text-[var(--leon-black)]/45">Net </span><b className={totalIn - totalOut < 0 ? 'text-[var(--leon-red)]' : ''}>{fmtMoney(totalIn - totalOut)}</b></span>
        </div>
      </div>

      {!project && (
        <div className="flex items-center gap-2 flex-wrap mb-3 px-3 py-2 bg-[var(--leon-cream)] rounded-lg text-xs">
          <span className="font-bold uppercase tracking-wide text-[var(--leon-black)]/50 shrink-0">Opening balance</span>
          <TextInput type="number" value={ctx.cashSettings.openingBalance}
            onChange={e => ctx.updateCashSettings({ openingBalance: Number(e.target.value) || 0 })}
            className="!w-32 !py-1 !text-xs" />
          <span className="text-[var(--leon-black)]/45">as of</span>
          <TextInput type="date" value={ctx.cashSettings.openingDate}
            onChange={e => ctx.updateCashSettings({ openingDate: e.target.value })}
            className="!w-36 !py-1 !text-xs" />
          <span className="text-[var(--leon-black)]/40">Each period then opens where the last one closed &mdash; use <b>Set balance</b> in the detail below to correct it from a point onward.</span>
        </div>
      )}
      <div className="overflow-x-auto border border-[var(--leon-line)] rounded-xl bg-white">
        <div className="flex" style={{ minWidth: dayMode ? 720 : Math.max(720, weeks * 110) }}>
          {buckets.map((b, i) => {
            const isNow = today >= b.start && today <= b.end;
            const negative = b.running < 0;
            return (
              <button key={b.start} onClick={() => setOpenWeek(openWeek === i ? null : i)}
                className={`flex-1 border-r border-[var(--leon-line)] last:border-r-0 p-2 text-left transition ${openWeek === i ? 'bg-[var(--leon-cream)]' : 'hover:bg-[var(--leon-cream)]/60'} ${isNow ? 'ring-1 ring-inset ring-[var(--leon-brown)]' : ''}`}>
                <div className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45 mb-1">
                  {b.isDay
                    ? `${fromISO(b.start).toLocaleDateString('en-US', { weekday: 'short' })} ${fromISO(b.start).getDate()}`
                    : (isNow ? 'This week' : fmtDate(b.start).replace(/,.*$/, ''))}
                </div>
                {/* Paired bars: in above the line, out below — the shape of the
                    week is readable before any number is. */}
                <div className="h-16 flex flex-col justify-center gap-0.5 mb-1">
                  {showKind !== 'out' && (
                    <div className="h-6 rounded-sm" title={`In ${fmtMoney(b.in)}`}
                         style={{ width: `${Math.max(b.in > 0 ? 6 : 0, (b.in / maxBar) * 100)}%`, background: scheduleGradient('Complete') }} />
                  )}
                  {showKind !== 'in' && (
                    <div className="h-6 rounded-sm" title={`Out ${fmtMoney(b.out)}`}
                         style={{ width: `${Math.max(b.out > 0 ? 6 : 0, (b.out / maxBar) * 100)}%`, background: scheduleGradient('Delayed') }} />
                  )}
                </div>
                <div className="text-[11px] tabular-nums leading-tight">
                  <div style={{ color: scheduleColor('Complete') }}>{b.in ? fmtMoney(b.in) : '—'}</div>
                  <div style={{ color: scheduleColor('Delayed') }}>{b.out ? fmtMoney(b.out) : '—'}</div>
                  <div className={`font-bold border-t border-[var(--leon-line)] mt-0.5 pt-0.5 ${negative ? 'text-[var(--leon-red)]' : ''}`}
                       title={`Opens ${fmtMoney(b.opening ?? 0)} → closes ${fmtMoney(b.running)}`}>
                    {fmtMoney(b.running)}
                    {b.overridden && <span className="ml-1 text-[9px] font-normal text-[var(--leon-brown)]">set</span>}
                  </div>
                </div>
                <div className="text-[10px] text-[var(--leon-black)]/35 mt-0.5 flex items-center gap-1">
                  <span>{b.events.length || 'no'} item{b.events.length === 1 ? '' : 's'}</span>
                  {(() => {
                    // A glanceable roll-up of what needs attention in this column.
                    const f = b.events.map(e => cashEventFlag(e, today));
                    const late = f.filter(x => x.level === 'late').length;
                    const soon = f.filter(x => x.level === 'soon').length;
                    const done = f.filter(x => x.level === 'settled').length;
                    return (<>
                      {done > 0 && <span className="text-[var(--leon-green)] font-bold" title={`${done} settled`}>&#10003;{done > 1 ? done : ''}</span>}
                      {soon > 0 && <span className="text-[var(--leon-yellow)] font-bold" title={`${soon} coming due`}>&#9888;{soon > 1 ? soon : ''}</span>}
                      {late > 0 && <span className="text-[var(--leon-red)] font-bold" title={`${late} overdue`}>!{late > 1 ? late : ''}</span>}
                    </>);
                  })()}
                </div>
              </button>
            );
          })}
        </div>
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-2">
        The bottom figure is each week&rsquo;s closing balance, carried from the opening balance above.
        It only reflects what is entered here &mdash; overheads you haven&rsquo;t added aren&rsquo;t counted.
      </p>

      {/* By-day view: the day you click on opens underneath, so the week stays
          a compact strip until you ask for a particular day. */}
      {dayMode && openWeek == null && (
        <p className="text-xs text-[var(--leon-black)]/40 mt-3">Click a day above to see what comes in and goes out that day.</p>
      )}
      {dayMode && openWeek != null && buckets[openWeek] && (() => {
        const b = buckets[openWeek];
        const evts = b.events.filter(e => showKind === 'all' || e.direction === showKind);
        const isToday = b.start === today;
        return (
          <div className={`mt-4 border rounded-xl overflow-hidden ${isToday ? 'border-[var(--leon-brown)]' : 'border-[var(--leon-line)]'}`}>
            <div className={`flex items-center gap-3 flex-wrap px-3 py-2 ${isToday ? 'bg-[var(--leon-cream)]' : 'bg-white'} border-b border-[var(--leon-line)]`}>
              <h4 className="text-sm font-bold">
                {fromISO(b.start).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
                {isToday && <span className="ml-2 text-[10px] uppercase tracking-wide text-[var(--leon-brown)]">Today</span>}
              </h4>
              {evts.length > 0 && (
                <span className="text-xs tabular-nums">
                  {b.in > 0 && <b style={{ color: scheduleColor('Complete') }}>+{fmtMoney(b.in)}</b>}
                  {b.in > 0 && b.out > 0 && <span className="text-[var(--leon-black)]/30"> / </span>}
                  {b.out > 0 && <b style={{ color: scheduleColor('Delayed') }}>&minus;{fmtMoney(b.out)}</b>}
                </span>
              )}
              <div className="flex-1" />
              {!project && (
                <>
                  <span className="text-xs text-[var(--leon-black)]/45 tabular-nums">
                    opens {fmtMoney(b.opening ?? 0)} &rarr; closes <b className={b.running < 0 ? 'text-[var(--leon-red)]' : ''}>{fmtMoney(b.running)}</b>
                  </span>
                  <button
                    onClick={() => {
                      const v = prompt('Set the balance at the start of this day (blank to go back to carrying forward):', String(b.opening ?? 0));
                      if (v === null) return;
                      ctx.setWeekOpeningBalance(b.start, v.trim() === '' ? null : v);
                    }}
                    className="text-xs text-[var(--leon-brown)] font-semibold">
                    {b.overridden ? 'Change balance' : 'Set balance'}
                  </button>
                </>
              )}
              <button onClick={() => setOpenWeek(null)} className="text-xs text-[var(--leon-black)]/40 hover:underline">Close</button>
            </div>
            <div className="p-2">
              <CashEventList events={evts} ctx={ctx} emptyText="Nothing due on this day." />
            </div>
          </div>
        );
      })()}

      {!dayMode && openWeek != null && buckets[openWeek] && (
        <div className="mt-4">
          <div className="flex items-center gap-2 mb-2 flex-wrap">
            <h3 className="text-sm font-bold">
              {buckets[openWeek].isDay
                ? fmtDate(buckets[openWeek].start)
                : `${fmtDate(buckets[openWeek].start)} – ${fmtDate(buckets[openWeek].end)}`}
            </h3>
            {!project && (
              <>
                <span className="text-xs text-[var(--leon-black)]/45">
                  opens {fmtMoney(buckets[openWeek].opening ?? 0)} &rarr; closes {fmtMoney(buckets[openWeek].running)}
                </span>
                <button
                  onClick={() => {
                    const cur = buckets[openWeek].opening ?? 0;
                    const v = prompt(`Set the balance at the start of this ${buckets[openWeek].isDay ? 'day' : 'week'} (blank to go back to carrying forward):`, String(cur));
                    if (v === null) return;
                    ctx.setWeekOpeningBalance(buckets[openWeek].start, v.trim() === '' ? null : v);
                  }}
                  className="text-xs text-[var(--leon-brown)] font-semibold">
                  {buckets[openWeek].overridden ? 'Change balance' : 'Set balance'}
                </button>
              </>
            )}
          </div>
          <CashEventList events={buckets[openWeek].events.filter(e => showKind === 'all' || e.direction === showKind)} ctx={ctx} />
        </div>
      )}
    </div>
  );
}

function CashEventList({ events, ctx, emptyText }) {
  const [postponing, setPostponing] = useState(null);
  if (!events.length) return <EmptyState text={emptyText || 'Nothing scheduled in this week.'} />;
  const today = todayISO();
  // Only Accounting/Admin move planned money around — everyone else reads it.
  const canMove = ctx.canSeeAccountingHub;
  return (
    <div className="space-y-1">
      {events.map(e => {
        const flag = cashEventFlag(e, today);
        const movable = canMove && !e.actual && ['paymentTerm', 'apInvoice', 'bankRelease'].includes(e.sourceType);
        return (
          <div key={e.id} className={`flex items-center gap-3 border rounded-lg px-3 py-2 bg-white ${flag.level === 'late' ? 'border-[var(--leon-red)]/50' : flag.level === 'soon' ? 'border-[var(--leon-yellow)]/60' : 'border-[var(--leon-line)]'}`}>
            <span className="w-1.5 self-stretch rounded-sm shrink-0"
                  style={{ background: e.direction === 'in' ? scheduleColor('Complete') : scheduleColor('Delayed') }} />
            {flag.icon && (
              <span title={flag.label}
                className={`shrink-0 w-5 h-5 rounded-full grid place-items-center text-[11px] font-bold ${
                  flag.tone === 'green' ? 'bg-[var(--leon-green)]/15 text-[var(--leon-green)]'
                  : flag.tone === 'red' ? 'bg-[var(--leon-red)]/15 text-[var(--leon-red)]'
                  : 'bg-[var(--leon-yellow)]/20 text-[var(--leon-yellow)]'}`}>{flag.icon}</span>
            )}
            <div className="min-w-0 flex-1">
              <p className="text-sm font-semibold truncate">
                {e.label}
                {/* An estimate must never look like a commitment. */}
                {!e.actual && e.dateSource === 'derived' && <span className="ml-2 text-[10px] font-normal text-[var(--leon-black)]/40 italic">estimated from schedule</span>}
                {e.actual && <Badge tone="green">{e.direction === 'out' || e.bankHeld ? 'Paid' : 'Received'}</Badge>}
                {e.blocked && <Badge tone="yellow">{e.blocked}</Badge>}
                {e.postponed && <span className="ml-2 text-[10px] font-normal text-[var(--leon-black)]/40" title={`Originally ${fmtDate(e.postponed)}`}>moved from {fmtDate(e.postponed)}</span>}
              </p>
              <p className="text-xs text-[var(--leon-black)]/50 truncate">
                {[e.kind, e.projectName, e.detail].filter(Boolean).join(' \u00b7 ')}
              </p>
            </div>
            {movable && (
              <button onClick={() => setPostponing(e)} className="text-xs text-[var(--leon-brown)] font-semibold shrink-0 hover:underline">
                Postpone
              </button>
            )}
            <div className="text-right shrink-0">
              <p className="text-sm font-bold tabular-nums" style={{ color: e.direction === 'in' ? scheduleColor('Complete') : scheduleColor('Delayed') }}>
                {e.direction === 'in' ? '+' : '\u2212'}{fmtMoney(e.amount)}
              </p>
              <p className={`text-[10px] ${flag.level === 'late' ? 'text-[var(--leon-red)] font-semibold' : flag.level === 'soon' ? 'text-[var(--leon-yellow)] font-semibold' : 'text-[var(--leon-black)]/40'}`}>
                {fmtDate(e.date)}{flag.level === 'late' || flag.level === 'soon' ? ` \u00b7 ${flag.label}` : ''}
              </p>
            </div>
          </div>
        );
      })}
      <PostponeCashDateModal ctx={ctx} event={postponing} onClose={() => setPostponing(null)} />
    </div>
  );
}

// Recording that the client paid — and, when the bank sits on the money,
// the schedule by which it is actually released. Same modal for a payment term
// and for an AIA requisition; only the label differs.
function RecordClientReceiptModal({ open, onClose, ctx, projectId, kind, record, label, defaultAmount }) {
  const [date, setDate] = useState('');
  const [amount, setAmount] = useState('');
  const [reference, setReference] = useState('');
  const [held, setHeld] = useState(false);
  const [bankName, setBankName] = useState('');
  const [releases, setReleases] = useState([]);
  useEffect(() => {
    if (!open || !record) return;
    setDate(record.receivedDate || todayISO());
    setAmount(String(record.receivedAmount != null ? record.receivedAmount : (defaultAmount || '')));
    setReference(record.receiptReference || '');
    const h = record.bankHold;
    setHeld(!!(h && h.held));
    setBankName((h && h.bankName) || '');
    setReleases(h && h.releases ? h.releases.map(r => ({ ...r })) : []);
  }, [open, record]);
  if (!record) return null;

  const total = Number(amount) || 0;
  const allocated = releases.reduce((n, r) => n + (Number(r.amount) || 0), 0);
  const left = Math.round((total - allocated) * 100) / 100;
  const balanced = Math.abs(left) < 0.005;

  function preset(n) {
    const parts = splitBankHoldAmounts(total, n);
    setReleases(parts.map((amt, i) => makeBankRelease({ name: `Release ${i + 1}`, amount: amt, plannedDate: '' })));
  }
  function addPhase() {
    setReleases(prev => [...prev, makeBankRelease({ name: `Release ${prev.length + 1}`, amount: left > 0 ? left : 0, plannedDate: '' })]);
  }
  function setPhase(i, fields) { setReleases(prev => prev.map((r, j) => j === i ? { ...r, ...fields } : r)); }
  const missingDate = held && releases.some(r => !r.plannedDate && !r.releasedDate);
  const canSave = !!date && total > 0 && (!held || (releases.length > 0 && balanced && !missingDate));

  function submit() {
    ctx.recordClientReceipt(projectId, kind, record.id, { date, amount: total, reference });
    ctx.setBankHold(projectId, kind, record.id,
      held ? makeBankHold({ bankName, releases: releases.map(r => ({ ...r, amount: Number(r.amount) || 0 })) }) : null);
    onClose();
  }

  return (
    <Modal wide open={open} onClose={onClose} title={`Record payment — ${label}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!canSave}>Record payment</Button></>}>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-3 gap-3">
          <Field label="Date paid" hint="The day the client paid"><TextInput type="date" value={date} onChange={e => setDate(e.target.value)} /></Field>
          <Field label="Amount received"><TextInput type="number" value={amount} onChange={e => setAmount(e.target.value)} /></Field>
          <Field label="Reference" hint="Wire / check no."><TextInput value={reference} onChange={e => setReference(e.target.value)} /></Field>
        </div>

        <label className="flex items-start gap-2 border border-[var(--leon-line)] rounded-lg px-3 py-2 cursor-pointer">
          <input type="checkbox" checked={held} onChange={e => { setHeld(e.target.checked); if (e.target.checked && !releases.length) preset(2); }}
                 className="w-4 h-4 accent-[var(--leon-brown)] mt-0.5" />
          <span>
            <span className="text-sm font-semibold">Deposit held by the bank</span>
            <span className="block text-xs text-[var(--leon-black)]/50">
              The payment is recorded on the day above but shows on the calendar as <b>$0</b> &mdash; the money
              arrives on the release dates below instead. Internal control only; the client still sees it as paid.
            </span>
          </span>
        </label>

        {held && (
          <div className="border border-[var(--leon-line)] rounded-lg p-3 space-y-3">
            <div className="flex items-end gap-3 flex-wrap">
              <Field label="Held by" className="!mb-0"><TextInput value={bankName} onChange={e => setBankName(e.target.value)} placeholder="Bank name" className="!w-48" /></Field>
              <div className="flex items-center gap-1.5">
                <span className="text-[10px] uppercase tracking-wide font-bold text-[var(--leon-black)]/45">Split</span>
                {[2, 3, 4].map(n => (
                  <Button key={n} size="sm" variant="ghost" onClick={() => preset(n)}>{n} phases</Button>
                ))}
              </div>
              <div className="flex-1" />
              <Button size="sm" variant="ghost" onClick={addPhase}>+ Add phase</Button>
            </div>

            <div className="space-y-1.5">
              {releases.map((r, i) => (
                <div key={r.id} className="flex items-end gap-2 flex-wrap">
                  <Field label={i === 0 ? 'Phase' : ''} className="!mb-0">
                    <TextInput value={r.name} onChange={e => setPhase(i, { name: e.target.value })} className="!w-40 !py-1 !text-xs" />
                  </Field>
                  <Field label={i === 0 ? 'Release date' : ''} className="!mb-0">
                    <TextInput type="date" value={r.plannedDate || ''} onChange={e => setPhase(i, { plannedDate: e.target.value })} className="!w-36 !py-1 !text-xs" />
                  </Field>
                  <Field label={i === 0 ? 'Amount' : ''} className="!mb-0">
                    <TextInput type="number" value={r.amount} onChange={e => setPhase(i, { amount: e.target.value })} className="!w-32 !py-1 !text-xs" />
                  </Field>
                  {left !== 0 && (
                    <button onClick={() => setPhase(i, { amount: Math.round(((Number(r.amount) || 0) + left) * 100) / 100 })}
                      className="text-[11px] text-[var(--leon-brown)] font-semibold pb-2" title="Absorb the unallocated remainder into this phase">
                      use remainder
                    </button>
                  )}
                  <IconBtn title="Remove phase" onClick={() => setReleases(prev => prev.filter((_, j) => j !== i))}>&#10005;</IconBtn>
                </div>
              ))}
            </div>

            {/* The reconciliation IS the control — saving is blocked until the
                phases account for every dollar received. */}
            <div className={`text-sm px-3 py-2 rounded-lg ${balanced ? 'bg-[var(--leon-green)]/10 text-[var(--leon-green)]' : 'bg-[var(--leon-yellow)]/15 text-[var(--leon-yellow)]'} font-semibold tabular-nums`}>
              Received {fmtMoney(total)} &middot; allocated {fmtMoney(allocated)}
              {balanced ? ' \u00b7 fully allocated \u2713' : ` \u00b7 ${fmtMoney(Math.abs(left))} ${left > 0 ? 'left to assign' : 'over-allocated'}`}
            </div>
            {missingDate && <p className="text-xs text-[var(--leon-red)] font-semibold">Every phase needs a release date.</p>}
          </div>
        )}
      </div>
    </Modal>
  );
}

// The release schedule as it stands, wherever the receivable is shown.
function BankHoldPanel({ ctx, projectId, kind, record, compact }) {
  const [releasing, setReleasing] = useState(null);
  const [postponing, setPostponing] = useState(null);
  const hold = record.bankHold;
  if (!hold || !hold.held) return null;
  const total = record.receivedAmount != null ? Number(record.receivedAmount) : 0;
  const released = bankHoldReleased(hold);
  const today = todayISO();
  const canEdit = ctx.canSeeAccountingHub;
  return (
    <div className="mt-2 border border-[var(--leon-brown)]/30 rounded-lg bg-[var(--leon-cream)]/60 p-2.5">
      <div className="flex items-center gap-2 flex-wrap mb-1.5">
        <Badge tone="yellow">Held by bank</Badge>
        <span className="text-xs text-[var(--leon-black)]/55">
          {hold.bankName ? `${hold.bankName} · ` : ''}{fmtMoney(released)} of {fmtMoney(total)} released
        </span>
        {!bankHoldBalanced(hold, total) && (
          <span className="text-xs text-[var(--leon-red)] font-semibold">
            phases total {fmtMoney(bankHoldAllocated(hold))} — does not match the payment
          </span>
        )}
      </div>
      <div className="space-y-1">
        {(hold.releases || []).map((r, i) => {
          const done = !!r.releasedDate;
          const flag = cashEventFlag({ actual: done, date: done ? r.releasedDate : r.plannedDate, direction: 'in' }, today);
          return (
            <div key={r.id} className="flex items-center gap-2 text-xs bg-white border border-[var(--leon-line)] rounded px-2 py-1.5 flex-wrap">
              {flag.icon && (
                <span title={flag.label} className={`w-4 h-4 rounded-full grid place-items-center text-[10px] font-bold shrink-0 ${
                  flag.tone === 'green' ? 'bg-[var(--leon-green)]/15 text-[var(--leon-green)]'
                  : flag.tone === 'red' ? 'bg-[var(--leon-red)]/15 text-[var(--leon-red)]'
                  : 'bg-[var(--leon-yellow)]/20 text-[var(--leon-yellow)]'}`}>{flag.icon}</span>
              )}
              <span className="font-semibold">{r.name || `Release ${i + 1}`}</span>
              <span className="text-[var(--leon-black)]/50">
                {done ? `released ${fmtDate(r.releasedDate)}` : `planned ${r.plannedDate ? fmtDate(r.plannedDate) : '—'}`}
                {!done && (flag.level === 'late' || flag.level === 'soon') && ` · ${flag.label}`}
              </span>
              {r.plannedDateOriginal && r.plannedDateOriginal !== r.plannedDate && !done && (
                <span className="text-[10px] text-[var(--leon-black)]/40">moved from {fmtDate(r.plannedDateOriginal)}</span>
              )}
              <div className="flex-1" />
              <span className="font-bold tabular-nums">
                {fmtMoney(done && r.releasedAmount != null ? r.releasedAmount : r.amount)}
                {done && r.releasedAmount != null && Math.abs(r.releasedAmount - (Number(r.amount) || 0)) > 0.005 && (
                  <span className="ml-1 font-normal text-[var(--leon-black)]/40">of {fmtMoney(r.amount)} planned</span>
                )}
              </span>
              {canEdit && !compact && (done
                ? <button onClick={() => ctx.undoBankRelease(projectId, kind, record.id, r.id)} className="text-[var(--leon-black)]/40 hover:underline">undo</button>
                : <>
                    <button onClick={() => setPostponing(r)} className="text-[var(--leon-brown)] font-semibold hover:underline">postpone</button>
                    <button onClick={() => setReleasing(r)} className="text-[var(--leon-brown)] font-semibold hover:underline">record release</button>
                  </>)}
            </div>
          );
        })}
      </div>

      <Modal open={!!releasing} onClose={() => setReleasing(null)} title="Record bank release"
        footer={<><Button variant="ghost" onClick={() => setReleasing(null)}>Cancel</Button>
                 <Button onClick={() => {
                   ctx.recordBankRelease(projectId, kind, record.id, releasing.id,
                     { releasedDate: releasing._d || todayISO(), releasedAmount: releasing._a != null ? releasing._a : releasing.amount });
                   setReleasing(null);
                 }}>Record</Button></>}>
        {releasing && (
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Released on"><TextInput type="date" defaultValue={releasing.plannedDate || todayISO()} onChange={e => { releasing._d = e.target.value; }} /></Field>
            <Field label="Amount released" hint="Change it if the bank released a different figure">
              <TextInput type="number" defaultValue={releasing.amount} onChange={e => { releasing._a = Number(e.target.value); }} />
            </Field>
          </div>
        )}
      </Modal>

      <Modal open={!!postponing} onClose={() => setPostponing(null)} title="Move release date"
        footer={<><Button variant="ghost" onClick={() => setPostponing(null)}>Cancel</Button>
                 <Button onClick={() => {
                   ctx.postponeBankRelease(projectId, kind, record.id, postponing.id, postponing._d || postponing.plannedDate, postponing._r || '');
                   setPostponing(null);
                 }}>Move date</Button></>}>
        {postponing && (
          <div className="space-y-3">
            <Field label="New release date"><TextInput type="date" defaultValue={postponing.plannedDate || todayISO()} onChange={e => { postponing._d = e.target.value; }} /></Field>
            <Field label="Why is it moving?" hint="Goes on the project change log">
              <TextInput defaultValue="" onChange={e => { postponing._r = e.target.value; }} placeholder="e.g. bank pushed the second tranche a week" />
            </Field>
            <ForecastDateHistory history={postponing.plannedDateHistory} label="Moved" />
          </div>
        )}
      </Modal>
    </div>
  );
}

// Every move of a forecast date, oldest first. The plan is allowed to change;
// what it must never do is change silently.
function ForecastDateHistory({ history, label }) {
  const rows = history || [];
  if (!rows.length) return null;
  return (
    <div className="mt-2">
      <p className="text-[10px] font-bold uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">{label || 'Date history'} ({rows.length})</p>
      <ul className="space-y-0.5">
        {rows.map((h, i) => (
          <li key={i} className="text-[11px] text-[var(--leon-black)]/55 leading-snug">
            <span className="tabular-nums">{h.from ? fmtDate(h.from) : 'unset'}</span>
            <span className="mx-1 text-[var(--leon-black)]/30">&rarr;</span>
            <b className="tabular-nums">{h.to ? fmtDate(h.to) : 'back to schedule'}</b>
            <span className="text-[var(--leon-black)]/35"> &middot; {h.by} on {fmtDate(h.date)}</span>
            {h.reason && <span className="text-[var(--leon-black)]/45"> &mdash; {h.reason}</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

// Moving a planned date is a forecasting decision, not a correction — it asks
// for a reason so the change log explains why the plan moved.
function PostponeCashDateModal({ ctx, event, onClose }) {
  const [date, setDate] = useState('');
  const [reason, setReason] = useState('');
  useEffect(() => { if (event) { setDate(event.date || todayISO()); setReason(''); } }, [event]);
  if (!event) return null;
  const isIn = event.direction === 'in';
  function submit() {
    if (!date) return;
    if (event.sourceType === 'paymentTerm') ctx.postponePaymentTerm(event.projectId, event.sourceId, date, reason);
    else if (event.sourceType === 'bankRelease') ctx.postponeBankRelease(event.projectId, event.parentType, event.parentId, event.sourceId, date, reason);
    else ctx.postponeApInvoiceDue(event.projectId, event.sourceId, date, reason);
    onClose();
  }
  return (
    <Modal open={!!event} onClose={onClose}
      title={event.sourceType === 'bankRelease' ? 'Move bank release date' : isIn ? 'Move expected payment date' : 'Move vendor payment date'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!date}>Move date</Button></>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/55">
          <b>{event.label}</b> &mdash; {fmtMoney(event.amount)}{event.projectName ? ` \u00b7 ${event.projectName}` : ''}.
          Currently planned for {fmtDate(event.date)}. This only moves the <i>forecast</i>;
          {isIn ? ' it locks to the real date once the payment is recorded.' : ' the invoice itself is unchanged.'}
        </p>
        <Field label="New date"><TextInput type="date" value={date} onChange={e => setDate(e.target.value)} /></Field>
        <Field label="Why is it moving?" hint="Goes on the project change log">
          <TextInput value={reason} onChange={e => setReason(e.target.value)} placeholder="e.g. client confirmed funding lands the following week" />
        </Field>
      </div>
    </Modal>
  );
}

function AdminSettings({ ctx }) {
  if (!ctx.canManageCollection) return <LockedNotice label="LEON Collection is restricted." />;
  const [tab, setTab] = useHubSection(ctx, 'admin', 'scopes');
  const tabs = LEON_COLLECTION_TABS;
  // Read-only by handing the subtab a ctx with the two edit capabilities off,
  // rather than threading a flag through it and its three modals. Every control
  // in there already gates on these, so one override covers all of them and
  // none can be missed.
  const readOnlyCtx = useMemo(
    () => ({ ...ctx, canEditTariffClassification: false, canEditTariffShipmentInfo: false }),
    [ctx]);
  const active = tabs.find(t => t.key === tab) || tabs[0];
  return (
    <div>
      <h1 className="text-2xl font-bold mb-1">LEON Collection</h1>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4">Everything every project draws from &mdash; the scopes we sell, our suppliers&rsquo; finishes, our material specs, and the scheduling defaults behind them.</p>
      {/* A menu rather than a tab bar. These eight are not a row you scan across
          and compare — you come here to open ONE of them and work in it, and the
          bar had grown wide enough to wrap. The select names where you are, so
          nothing is lost by not seeing the other seven. */}
      <div className="flex flex-wrap items-center gap-2 mb-1 no-print">
        <label htmlFor="leon-collection-section" className="text-[11px] uppercase tracking-wide font-semibold text-[var(--leon-black)]/40">Section</label>
        <Select id="leon-collection-section" value={tab} onChange={e => setTab(e.target.value)} className="!w-72 !py-1.5 font-semibold">
          {tabs.map(t => <option key={t.key} value={t.key}>{t.icon} {t.label}</option>)}
        </Select>
      </div>
      <HubTools title={`LEON Collection — ${active.label}`} heading={`LEON Collection — ${active.label}`} />
      <div className="pt-4">
        {tab === 'scopes' && <AdminScopesTab ctx={ctx} />}
        {tab === 'finishes' && <SupplierCatalogBrowser ctx={ctx} editable />}
        {tab === 'materials' && <AdminMaterialsTab ctx={ctx} />}
        {tab === 'renders' && <RenderLibraryTab ctx={ctx} editable />}
        {tab === 'doors' && <DoorLibraryCollectionTab ctx={ctx} />}
        {tab === 'casework' && <CaseworkLibraryCollectionTab ctx={ctx} />}
        {tab === 'tiles' && <TileLibraryCollectionTab ctx={ctx} />}
        {tab === 'windows' && <WindowLibraryCollectionTab ctx={ctx} />}
        {tab === 'tariffs' && (
          <>
            <p className="text-sm text-[var(--leon-black)]/50 mb-4">
              Reference only &mdash; the same classifications the Export and Logistics screens read.
              Adding a code, changing a rate and recording duty are done from Logistics &rarr;
              Trade Compliance &amp; Tariffs.
            </p>
            <TariffLibrarySubTab ctx={readOnlyCtx} />
          </>
        )}
        {tab === 'salesTax' && <SalesTaxLibraryTab ctx={ctx} />}
        {/* `quoteDefaults` and `quoteLines` are the two keys this section used
            to have; a restored sessionStorage tab still resolves here. */}
        {(tab === 'quoteSettings' || tab === 'quoteDefaults' || tab === 'quoteLines') && (
          <QuoteSettingsSection ctx={ctx} start={tab === 'quoteDefaults' ? 'standards' : tab === 'quoteLines' ? 'lines' : 'analysis'} />
        )}
        {tab === 'leadTimes' && <AdminLeadTimesTab ctx={ctx} />}
      {tab === 'holidays' && <AdminHolidaysTab ctx={ctx} />}
        {tab === 'demo' && <AdminCompanyTab ctx={ctx} />}
      </div>
    </div>
  );
}

// Browse the imported supplier catalogs. Read-only on purpose: these are the
// suppliers' own published data, replaced by regenerating the catalog file,
// not edited per-browser. This is the answer to "where did the import go?" —
// the same records the Selection Hub's supplier picker searches.
// Sends selected supplier finishes into a scope-library selection category —
// e.g. twelve An Cuong woods onto Casework -> Wood Species. This is the bridge
// between a supplier's whole range and the handful LEON actually offers.
function AddFinishesToScopeModal({ open, onClose, ctx, items, onDone }) {
  const [famId, setFamId] = useState('');
  const [catId, setCatId] = useState('');
  const [newCat, setNewCat] = useState('');
  const fam = ctx.scopeLibrary.find(f => f.id === famId);
  useEffect(() => {
    if (!open) return;
    const first = ctx.scopeLibrary[0];
    setFamId(first ? first.id : ''); setCatId(''); setNewCat('');
  }, [open]);

  const target = fam && fam.categories.find(c => c.id === catId);
  // How many would actually be added — an option with the same name is skipped.
  const existing = new Set((target ? target.options : []).map(o => (o.name || '').trim().toLowerCase()));
  const fresh = (items || []).filter(i => i.name && !existing.has(i.name.trim().toLowerCase()));
  const dupes = (items || []).length - fresh.length;

  function submit() {
    if (!fam) return;
    let targetId = catId;
    if (!targetId && newCat.trim()) {
      ctx.lib.addCategoryWithOptions(fam.id, newCat.trim(), items);
      onDone();
      return;
    }
    if (!targetId) return;
    ctx.lib.addOptionsBulk(targetId, items);
    onDone();
  }

  return (
    <Modal open={open} onClose={onClose} wide title={`Add ${(items || []).length} finish${(items || []).length === 1 ? '' : 'es'} to a scope selection`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button>
               <Button onClick={submit} disabled={!fam || (!catId && !newCat.trim())}>
                 Add {fresh.length} option{fresh.length === 1 ? '' : 's'}
               </Button></>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/55">
          These become selectable finish options on every scope in that family &mdash; the picture and
          name come across, and the supplier&rsquo;s own catalog is left untouched.
        </p>
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Scope family">
            <Select value={famId} onChange={e => { setFamId(e.target.value); setCatId(''); }}>
              {ctx.scopeLibrary.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
            </Select>
          </Field>
          <Field label="Selection category">
            <Select value={catId} onChange={e => { setCatId(e.target.value); setNewCat(''); }}>
              <option value="">— new category —</option>
              {(fam ? fam.categories : []).map(c => <option key={c.id} value={c.id}>{c.name} ({c.options.length})</option>)}
            </Select>
          </Field>
        </div>
        {!catId && (
          <Field label="New category name" hint="e.g. Wood Species, Melamine Decor, Slab Colour">
            <TextInput value={newCat} onChange={e => setNewCat(e.target.value)} placeholder="Name the new selection category" />
          </Field>
        )}
        {dupes > 0 && (
          <p className="text-xs text-[var(--leon-yellow)] font-semibold">
            {dupes} already exist{dupes === 1 ? 's' : ''} in that category by name and will be skipped.
          </p>
        )}
        <div>
          <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">Selected</p>
          <div className="grid gap-1.5 max-h-56 overflow-y-auto [grid-template-columns:repeat(auto-fill,minmax(120px,1fr))]">
            {(items || []).map(i => (
              <div key={i.sup + i.id} className="border border-[var(--leon-line)] rounded overflow-hidden">
                {i.img
                  ? <Photo src={i.img} alt={i.name} title={i.name} className="w-full h-12 object-cover" />
                  : <div className="w-full h-12 bg-[var(--leon-cream)]" />}
                <p className="text-[10px] px-1 py-0.5 truncate" title={i.name}>{i.name}</p>
              </div>
            ))}
          </div>
        </div>
      </div>
    </Modal>
  );
}

// One catalog swatch. Read-only it is just a link to the supplier's page;
// editable it gains inline rename plus soft delete/restore.
// Picture-only viewer. Deliberately minimal: the swatch at full size, its
// identity, and one explicit link out — nothing to click through by accident.
function SupplierFinishViewer({ rec, onClose }) {
  useEffect(() => {
    function onKey(e) { if (e.key === 'Escape') onClose(); }
    window.addEventListener('keydown', onKey);
    // Stop the page behind from scrolling while the viewer is open.
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
  }, [onClose]);

  const specs = [
    ['Code', rec.code], ['Colour', rec.color], ['Surface', rec.surface],
    ['Style', rec.style], ['Collection', rec.collection], ['Construction', rec.construction],
    ['Fibre', rec.fiber], ['Dye', rec.dye], ['Backing', rec.backing],
    ['Thickness', rec.thickness], ['Size', rec.size], ['Pattern', rec.pattern],
  ].filter(([, v]) => v);

  return (
    <div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 no-print"
         onClick={onClose} role="dialog" aria-modal="true" aria-label={rec.name}>
      <div className="max-w-3xl w-full max-h-full overflow-y-auto bg-white rounded-xl" onClick={e => e.stopPropagation()}>
        <img src={rec.img} alt={rec.name} className="w-full max-h-[65vh] object-contain bg-[var(--leon-cream)] rounded-t-xl" />
        <div className="p-4">
          <div className="flex items-start justify-between gap-3 mb-2">
            <div className="min-w-0">
              <h3 className="text-lg font-bold leading-tight">{rec.name}</h3>
              <p className="text-xs text-[var(--leon-black)]/50">{[rec.supLabel, rec.cat].filter(Boolean).join(' · ')}</p>
            </div>
            <IconBtn title="Close" onClick={onClose}>✕</IconBtn>
          </div>
          {specs.length > 0 && (
            <dl className="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-xs mb-3">
              {specs.map(([k, v]) => (
                <div key={k} className="flex gap-2 border-b border-[var(--leon-line)] py-1">
                  <dt className="text-[var(--leon-black)]/45 w-24 shrink-0">{k}</dt>
                  <dd className="font-semibold min-w-0 break-words">{String(v)}</dd>
                </div>
              ))}
            </dl>
          )}
          {Array.isArray(rec.certs) && rec.certs.length > 0 && (
            <div className="flex flex-wrap gap-1 mb-3">
              {rec.certs.map(c => <Badge key={c} tone="green">{c}</Badge>)}
            </div>
          )}
          {rec.url && (
            <a href={rec.url} target="_blank" rel="noopener noreferrer"
               className="inline-block text-sm font-semibold text-[var(--leon-brown)] hover:underline">
              View on supplier site ↗
            </a>
          )}
        </div>
      </div>
    </div>
  );
}

function SupplierFinishCard({ ctx, rec, editable, selected, onToggle }) {
  const [editing, setEditing] = useState(false);
  // Clicking the swatch opens a picture-only viewer; the supplier's page is a
  // separate, explicit link underneath, so browsing never navigates away by
  // accident.
  const [viewing, setViewing] = useState(false);
  const [form, setForm] = useState({ name: rec.name || '', code: rec.code || '' });
  const hidden = !!rec.hidden;

  function save() {
    ctx.updateSupplierFinish(rec.sup, rec.id, { name: form.name.trim() || rec.name, code: form.code.trim() });
    setEditing(false);
  }
  const body = (
    <>
      {rec.img
        ? <button type="button" onClick={() => setViewing(true)} title="Click to view the picture"
            className={`block w-full ${hidden ? 'opacity-30' : ''} cursor-zoom-in`}>
            <img src={rec.img} alt="" loading="lazy" className="w-full h-24 object-cover" />
          </button>
        : <div className="w-full h-24 bg-[var(--leon-cream)] flex items-center justify-center text-[10px] text-[var(--leon-black)]/30">No image</div>}
      <div className="p-2">
        <div className="text-xs font-semibold truncate" title={rec.name}>{rec.name}</div>
        <div className="text-[10px] text-[var(--leon-black)]/45 truncate" title={rec.code}>{rec.code}</div>
        {(rec.color || rec.surface) && <div className="text-[10px] text-[var(--leon-black)]/35 truncate">{[rec.color, rec.surface].filter(Boolean).join(' · ')}</div>}
        {rec.style && <div className="text-[10px] text-[var(--leon-black)]/35 truncate">{rec.style}{rec.collection ? ` · ${rec.collection}` : ''}</div>}
      </div>
    </>
  );

  return (
    <div className={`relative border rounded-lg overflow-hidden bg-white transition ${selected ? 'border-[var(--leon-brown)] ring-2 ring-[var(--leon-brown)]/30' : hidden ? 'border-[var(--leon-red)]/40' : rec.edited ? 'border-[var(--leon-brown)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown-light)]'}`}>
      {onToggle && (
        <label className="absolute top-1 left-1 z-10 bg-white/90 rounded p-0.5 cursor-pointer" title="Select for a batch action">
          <input type="checkbox" checked={!!selected} onChange={onToggle} className="w-3.5 h-3.5 accent-[var(--leon-brown)] block" />
        </label>
      )}
      {editing ? (
        <div className="p-2 space-y-1.5">
          <TextInput autoFocus value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="Name" className="!py-1 !text-xs" />
          <TextInput value={form.code} onChange={e => setForm({ ...form, code: e.target.value })} placeholder="Code" className="!py-1 !text-xs" />
          <div className="flex gap-1">
            <Button size="sm" onClick={save}>Save</Button>
            <Button size="sm" variant="ghost" onClick={() => { setForm({ name: rec.name || '', code: rec.code || '' }); setEditing(false); }}>Cancel</Button>
          </div>
        </div>
      ) : body}
      {!editing && rec.url && (
        <div className="px-2 pb-1.5">
          <a href={rec.url} target="_blank" rel="noopener noreferrer"
             className="text-[10px] text-[var(--leon-brown)] font-semibold hover:underline">
            View on supplier site ↗
          </a>
        </div>
      )}
      {viewing && <SupplierFinishViewer rec={rec} onClose={() => setViewing(false)} />}
      {!editing && editable && (
        <div className="flex items-center gap-2 px-2 pb-2 text-[10px]">
          {hidden ? (
            <>
              <span className="text-[var(--leon-red)] font-semibold uppercase tracking-wide">Removed</span>
              <div className="flex-1" />
              <button onClick={() => ctx.setSupplierFinishHidden(rec.sup, rec.id, false)} className="text-[var(--leon-brown)] font-semibold">Restore</button>
            </>
          ) : (
            <>
              <button onClick={() => setEditing(true)} className="text-[var(--leon-brown)] font-semibold">Edit</button>
              {rec.edited && <button onClick={() => ctx.resetSupplierFinish(rec.sup, rec.id)} className="text-[var(--leon-black)]/45">Reset</button>}
              <div className="flex-1" />
              <button onClick={() => ctx.setSupplierFinishHidden(rec.sup, rec.id, true)} className="text-[var(--leon-red)] font-semibold">Remove</button>
            </>
          )}
        </div>
      )}
    </div>
  );
}

// Every finish catalog belongs to a vendor — the company we buy it from, raise
// POs against and pay. Until that link exists the catalog is just a name, so
// the unlinked ones are called out rather than left to be noticed.
// Bring a supplier's own finish list in, rather than waiting for it to be
// scripted into a finishes/*.js file. Imported finishes live in persisted
// state and merge into the same index the shipped catalogs use, so once
// imported they are indistinguishable in the picker, in search and on the
// vendor's page. Images come in as URLs — a browser cannot bundle the
// supplier's image files, and embedding thousands of them would not fit in
// localStorage, so a URL that is reachable renders and one that is not simply
// shows no swatch.
function ImportFinishesModal({ open, onClose, ctx, supKey }) {
  const [sup, setSup] = useState(supKey || '');
  const [raw, setRaw] = useState('');
  const [step, setStep] = useState('paste');
  const [headers, setHeaders] = useState([]);
  const [rows, setRows] = useState([]);
  const [map, setMap] = useState({});
  const fileRef = useRef(null);
  useEffect(() => { if (open) { setSup(supKey || ''); setRaw(''); setRows([]); setHeaders([]); setMap({}); setStep('paste'); } }, [open, supKey]);

  function read(text) {
    const { headers: h, rows: r } = parseHeaderCsv(text);
    setHeaders(h); setRows(r); setMap(guessColumnMap(h, FINISH_IMPORT_FIELDS)); setStep('map');
  }
  async function onFile(e) {
    const f = e.target.files[0];
    if (!f) return;
    const text = await f.text();
    setRaw(text); read(text); e.target.value = '';
  }
  const missing = FINISH_IMPORT_FIELDS.filter(f => f.required && !map[f.key]);
  const built = useMemo(() => {
    if (!sup || missing.length) return [];
    return rows.map(r => ({
      sup,
      name: r[map.name] || '', code: map.code ? r[map.code] : '',
      cat: r[map.cat] || 'Imported',
      collection: map.collection ? r[map.collection] : '',
      color: map.color ? r[map.color] : '',
      style: map.style ? r[map.style] : '',
      img: map.img ? r[map.img] : '',
    })).filter(x => x.name.trim());
  }, [rows, map, sup, missing.length]);
  // A code that already exists for this supplier is the same product coming in
  // twice, which is what happens when a price list is re-sent with additions.
  const existingCodes = useMemo(() => {
    if (!sup) return new Set();
    return new Set(supplierCatalog(true).filter(r => r.sup === sup && r.code).map(r => String(r.code).toLowerCase()));
  }, [sup, ctx.importedFinishes]);
  const dupes = built.filter(b => b.code && existingCodes.has(String(b.code).toLowerCase()));
  const fresh = built.filter(b => !(b.code && existingCodes.has(String(b.code).toLowerCase())));

  function commit() {
    const n = ctx.addImportedFinishes(fresh);
    alert(`${n} finish${n === 1 ? '' : 'es'} imported${dupes.length ? `, ${dupes.length} skipped as already present` : ''}.`);
    onClose();
  }

  return (
    <Modal wide open={open} onClose={onClose} title="Import finishes from a supplier"
      footer={step === 'map'
        ? <><Button variant="ghost" onClick={() => setStep('paste')}>Back</Button>
            <div className="flex-1" />
            <Button onClick={commit} disabled={!fresh.length}>
              {!sup ? 'Choose a supplier' : missing.length ? `Map ${missing.map(f => f.label).join(', ')}` : `Import ${fresh.length} finish${fresh.length === 1 ? '' : 'es'}`}
            </Button></>
        : <><Button variant="ghost" onClick={onClose}>Cancel</Button>
            <Button onClick={() => read(raw)} disabled={!raw.trim()}>Read the list</Button></>}>
      {step === 'paste' ? (
        <div className="space-y-3">
          <p className="text-sm text-[var(--leon-black)]/55">
            Upload or paste the supplier&rsquo;s list with a <b>header row</b> &mdash; the columns are matched
            by name, so the order does not matter and anything unrecognised is mapped by hand in the
            next step.
          </p>
          <Field label="Which supplier is this list from?" hint="It joins that supplier's catalog, and inherits the vendor already linked to it.">
            <Select value={sup} onChange={e => setSup(e.target.value)}>
              <option value="">— choose a supplier —</option>
              {SUPPLIER_CATALOGS.map(c => <option key={c.key} value={c.key}>{supplierDisplayName(c.key, ctx.vendors)}</option>)}
            </Select>
          </Field>
          <div className="flex items-center gap-2">
            <Button variant="outline" onClick={() => fileRef.current.click()}>&#11014; Upload CSV</Button>
            <input ref={fileRef} type="file" accept=".csv,text/csv,text/plain" className="hidden" onChange={onFile} />
            <span className="text-xs text-[var(--leon-black)]/40">or paste below</span>
          </div>
          <TextArea rows={8} value={raw} onChange={e => setRaw(e.target.value)}
            placeholder={'Name,Code,Category,Collection,Image\nCarrara Bianco,GSV-9101,Quartz Slab,Marble Look,https://…/9101.jpg'} />
        </div>
      ) : (
        <div className="space-y-3">
          {!rows.length ? <EmptyState text="Nothing readable in that — the first row must be the column headers." /> : (
            <>
              <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-2">
                {FINISH_IMPORT_FIELDS.map(f => (
                  <Field key={f.key} label={<span>{f.label}{f.required && <span className="text-[var(--leon-red)]"> *</span>}</span>}>
                    <Select value={map[f.key] || ''} onChange={e => setMap({ ...map, [f.key]: e.target.value })}
                      className={`!py-1 !text-xs ${f.required && !map[f.key] ? '!border-[var(--leon-yellow)]' : ''}`}>
                      <option value="">— not in this file —</option>
                      {headers.map(h => <option key={h} value={h}>{h}</option>)}
                    </Select>
                  </Field>
                ))}
              </div>
              <div className="flex items-center gap-3 flex-wrap text-xs">
                <span><b>{rows.length}</b> rows read</span>
                <span><b>{fresh.length}</b> will be imported</span>
                {dupes.length > 0 && <span className="text-[var(--leon-yellow)] font-semibold">{dupes.length} already present (matched on supplier code) &mdash; skipped</span>}
                {built.length < rows.length && <span className="text-[var(--leon-black)]/45">{rows.length - built.length} with no name &mdash; dropped</span>}
              </div>
              {fresh.length > 0 && (
                <div className="border border-[var(--leon-line)] rounded-xl overflow-x-auto max-h-72 overflow-y-auto">
                  <table className="w-full text-xs">
                    <thead className="sticky top-0 bg-[var(--leon-cream)]">
                      <tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50">
                        <th className="px-2 py-2">Name</th><th className="px-2 py-2">Code</th>
                        <th className="px-2 py-2">Category</th><th className="px-2 py-2">Collection</th><th className="px-2 py-2">Image</th>
                      </tr>
                    </thead>
                    <tbody>
                      {fresh.slice(0, 50).map((b, i) => (
                        <tr key={i} className="border-t border-[var(--leon-line)]">
                          <td className="px-2 py-1 font-semibold">{b.name}</td>
                          <td className="px-2 py-1">{b.code || '—'}</td>
                          <td className="px-2 py-1">{b.cat}</td>
                          <td className="px-2 py-1">{b.collection || '—'}</td>
                          <td className="px-2 py-1">{b.img ? <Photo src={b.img} alt={b.name} title={b.name} className="w-8 h-8 object-cover rounded" /> : '—'}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                  {fresh.length > 50 && <p className="px-2 py-1 text-[11px] text-[var(--leon-black)]/40">+{fresh.length - 50} more will be imported</p>}
                </div>
              )}
              <p className="text-[11px] text-[var(--leon-black)]/45">
                Imported finishes behave exactly like the shipped catalogs &mdash; searchable in the
                Selection Hub picker, editable and removable here, and listed on the linked
                vendor&rsquo;s page. <b>Images are referenced by URL</b>, not copied: a browser cannot
                bundle the supplier&rsquo;s image files, so a swatch shows only while its URL is reachable.
              </p>
            </>
          )}
        </div>
      )}
    </Modal>
  );
}

// Stock counted on a spreadsheet, brought in as real inventory rather than
// typed twice. Same header-mapping mechanism as the finish importer, because
// the problem is the same and one behaviour is easier to trust than two.
function ImportInventoryModal({ open, onClose, ctx, warehouseId }) {
  const [wh, setWh] = useState(warehouseId || '');
  const [raw, setRaw] = useState('');
  const [step, setStep] = useState('paste');
  const [headers, setHeaders] = useState([]);
  const [rows, setRows] = useState([]);
  const [map, setMap] = useState({});
  const [mode, setMode] = useState('add');   // add | update
  const fileRef = useRef(null);
  const warehouses = (ctx.warehouses || []).filter(w => w.active !== false);
  useEffect(() => { if (open) { setWh(warehouseId || (warehouses[0] || {}).id || ''); setRaw(''); setRows([]); setHeaders([]); setMap({}); setStep('paste'); setMode('add'); } }, [open, warehouseId]);

  function read(text) {
    const { headers: h, rows: r } = parseHeaderCsv(text);
    setHeaders(h); setRows(r); setMap(guessColumnMap(h, INVENTORY_IMPORT_FIELDS)); setStep('map');
  }
  async function onFile(e) {
    const f = e.target.files[0];
    if (!f) return;
    const text = await f.text();
    setRaw(text); read(text); e.target.value = '';
  }
  const missing = INVENTORY_IMPORT_FIELDS.filter(f => f.required && !map[f.key]);
  const built = useMemo(() => {
    if (!wh || missing.length) return [];
    const num = v => { const n = Number(String(v || '').replace(/[^0-9.\-]/g, '')); return isFinite(n) ? n : 0; };
    return rows.map(r => {
      const o = { warehouseId: wh };
      INVENTORY_IMPORT_FIELDS.forEach(f => { if (map[f.key]) o[f.key] = r[map[f.key]] || ''; });
      o.currentStock = num(o.currentStock);
      o.unitCost = num(o.unitCost);
      o.unitOfMeasure = o.unitOfMeasure || 'Units';
      return o;
    }).filter(x => String(x.name || '').trim());
  }, [rows, map, wh, missing.length]);
  // Matched on item no. first, then on name — an existing row is topped up
  // rather than duplicated, which is what a recount actually means.
  const existing = ctx.warehouseMaterials || [];
  function findExisting(b) {
    const id = String(b.itemId || '').trim().toLowerCase();
    if (id) { const m = existing.find(x => x.warehouseId === wh && String(x.itemId || '').trim().toLowerCase() === id); if (m) return m; }
    const nm = String(b.name || '').trim().toLowerCase();
    return existing.find(x => x.warehouseId === wh && String(x.name || '').trim().toLowerCase() === nm) || null;
  }
  const matched = built.map(b => ({ b, m: findExisting(b) }));
  const newOnes = matched.filter(x => !x.m);
  const updates = matched.filter(x => x.m);

  function commit() {
    const stamp = `Imported ${todayISO()}`;
    newOnes.forEach(({ b }) => ctx.addWarehouseMaterial({ ...b, importedFrom: stamp, dateReceived: todayISO() }));
    if (mode === 'update') {
      updates.forEach(({ b, m }) => ctx.adjustStock(m.id, b.currentStock, `Stock count imported ${todayISO()}`));
    }
    alert(`${newOnes.length} item${newOnes.length === 1 ? '' : 's'} added${mode === 'update' && updates.length ? `, ${updates.length} stock level${updates.length === 1 ? '' : 's'} updated` : updates.length ? `, ${updates.length} existing item${updates.length === 1 ? '' : 's'} left alone` : ''}.`);
    onClose();
  }

  return (
    <Modal wide open={open} onClose={onClose} title="Import inventory"
      footer={step === 'map'
        ? <><Button variant="ghost" onClick={() => setStep('paste')}>Back</Button>
            <div className="flex-1" />
            <Button onClick={commit} disabled={!built.length || (!newOnes.length && mode === 'add')}>
              {missing.length ? `Map ${missing.map(f => f.label).join(', ')}` : `Import ${newOnes.length}${mode === 'update' && updates.length ? ` + update ${updates.length}` : ''}`}
            </Button></>
        : <><Button variant="ghost" onClick={onClose}>Cancel</Button>
            <Button onClick={() => read(raw)} disabled={!raw.trim() || !wh}>Read the list</Button></>}>
      {step === 'paste' ? (
        <div className="space-y-3">
          <p className="text-sm text-[var(--leon-black)]/55">
            Upload or paste a stock list with a <b>header row</b>. Columns are matched by name; anything
            unrecognised is mapped by hand in the next step.
          </p>
          <Field label="Which warehouse is this stock in?">
            <Select value={wh} onChange={e => setWh(e.target.value)}>
              <option value="">— choose a warehouse —</option>
              {warehouses.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
            </Select>
          </Field>
          <div className="flex items-center gap-2">
            <Button variant="outline" onClick={() => fileRef.current.click()}>&#11014; Upload CSV</Button>
            <input ref={fileRef} type="file" accept=".csv,text/csv,text/plain" className="hidden" onChange={onFile} />
            <span className="text-xs text-[var(--leon-black)]/40">or paste below</span>
          </div>
          <TextArea rows={8} value={raw} onChange={e => setRaw(e.target.value)}
            placeholder={'Item Name,Item No,Category,Qty,Unit,Location,Unit Cost\nBlum Hinge 110°,BL-110,Hardware,240,Units,Rack A3,3.15'} />
        </div>
      ) : (
        <div className="space-y-3">
          {!rows.length ? <EmptyState text="Nothing readable in that — the first row must be the column headers." /> : (
            <>
              <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-2">
                {INVENTORY_IMPORT_FIELDS.map(f => (
                  <Field key={f.key} label={<span>{f.label}{f.required && <span className="text-[var(--leon-red)]"> *</span>}</span>}>
                    <Select value={map[f.key] || ''} onChange={e => setMap({ ...map, [f.key]: e.target.value })}
                      className={`!py-1 !text-xs ${f.required && !map[f.key] ? '!border-[var(--leon-yellow)]' : ''}`}>
                      <option value="">— not in this file —</option>
                      {headers.map(h => <option key={h} value={h}>{h}</option>)}
                    </Select>
                  </Field>
                ))}
              </div>
              {/* An item already in the warehouse is a recount, not a second
                  item. What should happen to it is a decision, not a default. */}
              {updates.length > 0 && (
                <Field label={`${updates.length} of these are already in this warehouse`} hint="Matched on item no., or on name where there is no item no.">
                  <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit">
                    {[{ k: 'add', l: 'Leave them as they are' }, { k: 'update', l: 'Set their stock to the imported count' }].map(o => (
                      <button key={o.k} type="button" onClick={() => setMode(o.k)}
                        className={`px-3 py-1.5 rounded-md text-xs font-semibold ${mode === o.k ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>{o.l}</button>
                    ))}
                  </div>
                </Field>
              )}
              <div className="flex items-center gap-3 flex-wrap text-xs">
                <span><b>{rows.length}</b> rows read</span>
                <span><b>{newOnes.length}</b> new item{newOnes.length === 1 ? '' : 's'}</span>
                {updates.length > 0 && <span><b>{updates.length}</b> already here</span>}
                {built.length < rows.length && <span className="text-[var(--leon-black)]/45">{rows.length - built.length} with no name &mdash; dropped</span>}
              </div>
              <div className="border border-[var(--leon-line)] rounded-xl overflow-x-auto max-h-72 overflow-y-auto">
                <table className="w-full text-xs">
                  <thead className="sticky top-0 bg-[var(--leon-cream)]">
                    <tr className="text-left text-[10px] font-bold uppercase text-[var(--leon-black)]/50">
                      <th className="px-2 py-2"></th><th className="px-2 py-2">Item</th><th className="px-2 py-2">Item no.</th>
                      <th className="px-2 py-2">Category</th><th className="px-2 py-2 text-right">Qty</th>
                      <th className="px-2 py-2">Unit</th><th className="px-2 py-2">Location</th>
                    </tr>
                  </thead>
                  <tbody>
                    {matched.slice(0, 60).map(({ b, m }, i) => (
                      <tr key={i} className={`border-t border-[var(--leon-line)] ${m && mode === 'add' ? 'opacity-40' : ''}`}>
                        <td className="px-2 py-1">{m ? <Badge tone="yellow">here</Badge> : <Badge tone="green">new</Badge>}</td>
                        <td className="px-2 py-1 font-semibold">{b.name}</td>
                        <td className="px-2 py-1">{b.itemId || '—'}</td>
                        <td className="px-2 py-1">{b.category || '—'}</td>
                        <td className="px-2 py-1 text-right tabular-nums">{b.currentStock}{m && mode === 'update' ? <span className="text-[var(--leon-black)]/40"> (was {m.currentStock})</span> : ''}</td>
                        <td className="px-2 py-1">{b.unitOfMeasure}</td>
                        <td className="px-2 py-1">{b.storageLocation || '—'}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                {matched.length > 60 && <p className="px-2 py-1 text-[11px] text-[var(--leon-black)]/40">+{matched.length - 60} more</p>}
              </div>
              <p className="text-[11px] text-[var(--leon-black)]/45">
                New items are created with the stock shown and stamped as imported. A stock change on
                an existing item is recorded as an <b>Adjustment</b> transaction, so the count history
                still explains itself.
              </p>
            </>
          )}
        </div>
      )}
    </Modal>
  );
}

// ── Add one supplier finish by hand ────────────────────────────────────────
// The catalogs arrive as bulk imports, but a product added one at a time — a
// trim profile, a one-off handle — had no way in except writing a CSV. This is
// the same record `addImportedFinishes` makes, so a hand-added finish is
// indistinguishable from an imported one everywhere it is read.
//
// The PHOTO is uploaded, not a URL. A bulk import references the supplier's own
// image server because thousands of data URIs would not fit in localStorage; one
// product at a time is a different problem, and asking someone to host a picture
// before they can record a trim is asking the wrong thing.
function AddFinishModal({ open, onClose, ctx, supKey, groups }) {
  const [f, setF] = useState({ sup: '', name: '', cat: '', code: '', collection: '', color: '', style: '', img: '' });
  const [newSup, setNewSup] = useState('');
  const set = (k, v) => setF(p => ({ ...p, [k]: v }));
  useEffect(() => { if (open) setF(p => ({ ...p, sup: supKey || '' })); }, [open, supKey]);

  const supplier = f.sup === '__new' ? newSup.trim() : f.sup;
  const ready = !!supplier && !!f.name.trim() && !!f.cat.trim();

  function save(again) {
    if (!ready) return;
    ctx.addImportedFinishes([{
      sup: supplier,
      supLabel: f.sup === '__new' ? newSup.trim() : undefined,
      name: f.name.trim(), cat: f.cat.trim(), code: f.code.trim(),
      collection: f.collection.trim(), color: f.color.trim(), style: f.style.trim(), img: f.img || '',
    }]);
    if (again) setF(p => ({ ...p, name: '', code: '', color: '', img: '' }));   // keep supplier + category
    else onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Add a finish">
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/55">
          One product, entered by hand. It joins the same catalog the imports land in, so it is
          offered in every finish picker &mdash; selections, quotations, door trims and hardware.
        </p>
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Supplier" hint="Which catalog it belongs to.">
            <Select value={f.sup} onChange={e => set('sup', e.target.value)}>
              <option value="">— choose —</option>
              {(groups || []).map(g => <option key={g.key} value={g.key}>{g.label}</option>)}
              <option value="__new">+ a supplier not listed…</option>
            </Select>
          </Field>
          {f.sup === '__new' && (
            <Field label="New supplier name">
              <TextInput value={newSup} onChange={e => setNewSup(e.target.value)} placeholder="Supplier" />
            </Field>
          )}
          <Field label="Name" hint="What it is called.">
            <TextInput value={f.name} onChange={e => set('name', e.target.value)} placeholder="e.g. Square Casing 2 1/2&quot;" />
          </Field>
          <Field label="Category" hint="How it is filed — Trim, Hardware, Laminate…">
            <TextInput value={f.cat} onChange={e => set('cat', e.target.value)} placeholder="Trim" />
          </Field>
          <Field label="Supplier code"><TextInput value={f.code} onChange={e => set('code', e.target.value)} /></Field>
          <Field label="Collection"><TextInput value={f.collection} onChange={e => set('collection', e.target.value)} /></Field>
          <Field label="Colour"><TextInput value={f.color} onChange={e => set('color', e.target.value)} /></Field>
          <Field label="Style / surface"><TextInput value={f.style} onChange={e => set('style', e.target.value)} /></Field>
          <Field label="Photo" className="sm:col-span-2"
            hint="Optional. Shown on the picker card and on a shop drawing's finishes strip.">
            <ImagePicker value={f.img} label="finish photo" onChange={v => set('img', v)} />
          </Field>
        </div>
        <div className="flex items-center gap-2 pt-1">
          <Button disabled={!ready} onClick={() => save(false)}>Add it</Button>
          <Button variant="outline" disabled={!ready} onClick={() => save(true)}>Add and enter another</Button>
          <span className="text-[11px] text-[var(--leon-black)]/45">
            {ready ? 'Supplier, name and category are all set.' : 'Supplier, name and category are needed.'}
          </span>
        </div>
      </div>
    </Modal>
  );
}

function SupplierVendorLinks({ ctx, editable, groups }) {
  // Every registered catalog, not just the ones with finishes loaded — a
  // supplier whose list has not arrived yet still needs its vendor link.
  const counts = {};
  groups.forEach(g => { counts[g.key] = g.cats.reduce((t, c) => t + c.count, 0); });
  const all = SUPPLIER_CATALOGS.map(c => ({ key: c.key, count: counts[c.key] || 0 }));
  const unlinked = all.filter(g => !supplierVendorId(g.key));
  function createVendorFor(g) {
    if (!confirm(`Create a vendor called "${supplierLabel(g.key)}" and link this catalog to it?`)) return;
    const v = ctx.addVendor({ name: supplierLabel(g.key), vendorType: 'Material Supplier', notes: 'Created from the finish catalog of the same name.' });
    ctx.linkSupplierVendor(g.key, v.id);
  }
  return (
    <Collapsible id="supplier-vendor-links" defaultOpen={editable && unlinked.length > 0}
      title={<span className="flex items-center gap-2">Suppliers &amp; vendors
        {unlinked.length > 0 && <Badge tone="yellow">{unlinked.length} not linked</Badge>}</span>}
      right={<span className="text-[11px] text-[var(--leon-black)]/45">{all.length - unlinked.length} of {all.length} linked</span>}>
      <p className="text-xs text-[var(--leon-black)]/50 mb-2 max-w-3xl">
        Which vendor each catalog is bought from. The link names the catalog everywhere it is
        offered, puts it on that vendor&rsquo;s own page, and is stamped onto a scope&rsquo;s selection
        so the scope still knows its supplier if the link is changed later.
      </p>
      <table className="w-full text-sm">
        <tbody>
          {all.map(g => {
            const id = supplierVendorId(g.key);
            const n = g.count;
            return (
              <tr key={g.key} className="border-b border-[var(--leon-line)] last:border-0">
                <td className="py-1.5 pr-3 font-semibold whitespace-nowrap">{supplierLabel(g.key)}</td>
                <td className="py-1.5 pr-3 text-xs text-[var(--leon-black)]/40 tabular-nums whitespace-nowrap">{n ? `${n} finishes` : 'no list imported yet'}</td>
                <td className="py-1.5 pr-3">
                  {editable ? (
                    <Select value={id || ''} onChange={e => ctx.linkSupplierVendor(g.key, e.target.value)}
                      className={`!py-1 !text-xs !w-64 ${id ? '' : '!border-[var(--leon-yellow)]'}`}>
                      <option value="">— not linked to a vendor —</option>
                      {ctx.vendors.filter(v => v.status !== 'Inactive').map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
                    </Select>
                  ) : (
                    <span className="text-xs">{id ? (ctx.vendors.find(v => v.id === id) || {}).name : <span className="text-[var(--leon-black)]/35">not linked</span>}</span>
                  )}
                </td>
                <td className="py-1.5 text-right">
                  {editable && !id && <Button size="sm" variant="ghost" onClick={() => createVendorFor(g)}>+ Create vendor</Button>}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </Collapsible>
  );
}

function SupplierCatalogBrowser({ ctx, editable }) {
  const groups = useMemo(() => supplierGroups(), [ctx.importedFinishes]);
  const [addOpen, setAddOpen] = useState(false);
  const [sup, setSup] = useState(() => (groups[0] || {}).key || '');
  const [cat, setCat] = useState('');
  const [q, setQ] = useState('');
  const [limit, setLimit] = useState(60);
  const group = groups.find(g => g.key === sup) || groups[0];

  const [showRemoved, setShowRemoved] = useState(false);
  const [importOpen, setImportOpen] = useState(false);
  const [onlyWithImage, setOnlyWithImage] = useState(false);
  // Batch selection — the catalogs run to thousands, so acting on one finish at
  // a time is unusable when the real task is "put these twelve woods on Casework".
  const [picked, setPicked] = useState(() => new Set());
  const [showAddTo, setShowAddTo] = useState(false);
  const results = useMemo(() => {
    if (!group) return [];
    const live = searchSupplierFinishes(group.key, cat || '', q, 5000);
    if (!editable || !showRemoved) return live;
    // Hidden records are excluded from the search index by design, so pull
    // them in separately when the admin wants to see what's been removed.
    const gone = supplierCatalog(true).filter(r => r.hidden && r.sup === group.key
      && (!cat || r.cat === cat)
      && (!q || (r.name || '').toLowerCase().includes(q.toLowerCase()) || (r.code || '').toLowerCase().includes(q.toLowerCase())));
    return live.concat(gone);
  }, [group, cat, q, editable, showRemoved, ctx.supplierFinishOverrides, ctx.importedFinishes]);
  const removedCount = useMemo(() => Object.values(ctx.supplierFinishOverrides || {}).filter(o => o && o.hidden).length, [ctx.supplierFinishOverrides]);
  const shown = (onlyWithImage ? results.filter(r => r.img) : results).slice(0, limit);

  useEffect(() => { setLimit(60); }, [sup, cat, q, onlyWithImage]);
  useEffect(() => { setPicked(new Set()); }, [sup]);

  if (!groups.length) return <EmptyState text="No supplier catalogs are loaded." />;

  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        {editable
          ? <>Finish catalogs imported from our suppliers &mdash; what the <b>+ Supplier finish</b> picker searches inside a scope&rsquo;s Selection Hub. Rename a finish or remove one we don&rsquo;t carry; the supplier&rsquo;s own record is kept underneath, so a re-import never wipes your changes and <b>Restore</b> brings it back.</>
          : <>Every finish available from our suppliers. Read-only here &mdash; edit or remove them in LEON Collection &rarr; Supplier Finishes.</>}
      </p>

      <SupplierVendorLinks ctx={ctx} editable={editable} groups={groups} />
      {editable && (
        <div className="flex justify-end gap-2 mb-2">
          {/* Adding ONE product by hand was only possible by writing a CSV,
              which is not what anyone means by entering something manually. */}
          <Button size="sm" onClick={() => setAddOpen(true)}>+ Add a finish</Button>
          <Button size="sm" variant="outline" onClick={() => setImportOpen(true)}>&#11014; Import finishes</Button>
        </div>
      )}
      <ImportFinishesModal open={importOpen} onClose={() => setImportOpen(false)} ctx={ctx} supKey={sup} />
      <AddFinishModal open={addOpen} onClose={() => setAddOpen(false)} ctx={ctx} supKey={sup} groups={groups} />

      <div className="flex flex-wrap gap-1 mb-3">
        {groups.map(g => {
          const n = g.cats.reduce((t, c) => t + c.count, 0);
          const vend = supplierVendorId(g.key) && ctx.vendors.find(v => v.id === supplierVendorId(g.key));
          return (
            <button key={g.key} onClick={() => { setSup(g.key); setCat(''); setQ(''); }}
              title={vend ? `Supplied by ${vend.name}` : 'Not linked to a vendor yet'}
              className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition ${sup === g.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] text-[var(--leon-brown)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:border-[var(--leon-brown-light)]'}`}>
              {vend ? vend.name : g.label} <span className="opacity-50">{n}</span>
              {!vend && <span className="ml-1 text-[var(--leon-yellow)]" title="Not linked to a vendor">•</span>}
            </button>
          );
        })}
      </div>

      <div className="flex items-center gap-2 mb-3 flex-wrap">
        <Select value={cat} onChange={e => setCat(e.target.value)} className="!py-1 !text-xs !w-64">
          <option value="">All constructions ({group.cats.reduce((t, c) => t + c.count, 0)})</option>
          {group.cats.map(c => <option key={c.cat} value={c.cat}>{c.cat} ({c.count})</option>)}
        </Select>
        <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search decor name or code…" className="!py-1 !text-xs !w-72" />
        <span className="text-xs text-[var(--leon-black)]/45">{results.length} {results.length === 1 ? 'finish' : 'finishes'}</span>
        {(() => {
          const withImg = results.filter(r => r.img).length;
          if (withImg === results.length || results.length === 0) return null;
          return (
            <label className="flex items-center gap-1.5 text-xs text-[var(--leon-black)]/60 cursor-pointer" title="Some suppliers publish images for only part of their range">
              <input type="checkbox" checked={onlyWithImage} onChange={e => setOnlyWithImage(e.target.checked)} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
              With picture only ({withImg})
            </label>
          );
        })()}
        {shown.length > 0 && (
          <button onClick={() => {
            const all = shown.every(r => picked.has(r.sup + ':' + r.id));
            setPicked(prev => {
              const next = new Set(prev);
              shown.forEach(r => { const k = r.sup + ':' + r.id; if (all) next.delete(k); else next.add(k); });
              return next;
            });
          }} className="text-xs text-[var(--leon-brown)] font-semibold">
            {shown.every(r => picked.has(r.sup + ':' + r.id)) ? 'Clear page' : `Select these ${shown.length}`}
          </button>
        )}
        {editable && removedCount > 0 && (
          <label className="flex items-center gap-1.5 text-xs text-[var(--leon-black)]/60 cursor-pointer">
            <input type="checkbox" checked={showRemoved} onChange={e => setShowRemoved(e.target.checked)} className="w-3.5 h-3.5 accent-[var(--leon-brown)]" />
            Show removed ({removedCount})
          </label>
        )}
      </div>

      {picked.size > 0 && (
        <div className="flex items-center gap-2 flex-wrap mb-3 px-3 py-2 rounded-lg bg-[var(--leon-cream)] border border-[var(--leon-brown)]/30">
          <span className="text-sm font-bold">{picked.size} selected</span>
          <Button size="sm" onClick={() => setShowAddTo(true)}>Add to a scope selection…</Button>
          {editable && (
            <>
              <Button size="sm" variant="ghost" onClick={() => {
                if (!confirm(`Remove ${picked.size} finish${picked.size === 1 ? '' : 'es'} from the catalog?`)) return;
                picked.forEach(k => { const [sp, ...rest] = k.split(':'); ctx.setSupplierFinishHidden(sp, rest.join(':'), true); });
                setPicked(new Set());
              }}>Remove selected</Button>
              <Button size="sm" variant="ghost" onClick={() => {
                picked.forEach(k => { const [sp, ...rest] = k.split(':'); ctx.resetSupplierFinish(sp, rest.join(':')); });
                setPicked(new Set());
              }}>Reset selected</Button>
            </>
          )}
          <div className="flex-1" />
          <button onClick={() => setPicked(new Set())} className="text-xs text-[var(--leon-black)]/50 hover:underline">Clear</button>
        </div>
      )}
      {shown.length === 0 ? <EmptyState text="No finishes match." /> : (
        <>
          <div className="grid gap-2 [grid-template-columns:repeat(auto-fill,minmax(150px,1fr))]">
            {shown.map(r => (
              <SupplierFinishCard key={r.sup + r.id} ctx={ctx} rec={r} editable={editable}
                selected={picked.has(r.sup + ':' + r.id)}
                onToggle={() => setPicked(prev => {
                  const k = r.sup + ':' + r.id; const next = new Set(prev);
                  if (next.has(k)) next.delete(k); else next.add(k);
                  return next;
                })} />
            ))}
          </div>
          {(onlyWithImage ? results.filter(r => r.img).length : results.length) > shown.length && (
            <div className="flex justify-center mt-3">
              <Button size="sm" variant="ghost" onClick={() => setLimit(l => l + 120)}>
                Show more ({(onlyWithImage ? results.filter(r => r.img).length : results.length) - shown.length} remaining)
              </Button>
            </div>
          )}
        </>
      )}
      <AddFinishesToScopeModal
        open={showAddTo}
        onClose={() => setShowAddTo(false)}
        ctx={ctx}
        items={supplierCatalog(true).filter(r => picked.has(r.sup + ':' + r.id))}
        onDone={() => { setShowAddTo(false); setPicked(new Set()); }}
      />
    </div>
  );
}

function AdminScopesTab({ ctx }) {
  const [newFamily, setNewFamily] = useState('');
  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        The scope families we sell, and the selection categories and finish options under each.
        Changes apply to every project going forward; selections already made are unaffected.
        A family flagged <b>Window Schedule</b> gets the specialized Window/Exterior Door System
        schedule instead of the normal stage list.
      </p>
      <div className="flex items-center gap-2 mb-4">
        <TextInput value={newFamily} onChange={e => setNewFamily(e.target.value)} placeholder="New scope family name" className="!w-64" />
        <Button onClick={() => { if (newFamily.trim()) { ctx.lib.addFamily(newFamily.trim()); setNewFamily(''); } }}>+ Add Scope Family</Button>
      </div>
      {ctx.scopeLibrary.map((fam, fi) => (
        <FamilyEditor key={fam.id} ctx={ctx} fam={fam} isFirst={fi === 0} isLast={fi === ctx.scopeLibrary.length - 1} />
      ))}
    </div>
  );
}

function AdminMaterialsTab({ ctx }) {
  return <MaterialsSubTab ctx={ctx} />;
}

// The holiday table. Editable on purpose: this ships with the app rather than
// coming from a feed, the lunar and Islamic dates move every year, and Islamic
// dates additionally depend on the moon sighting and differ country to
// country. Correcting them here is the answer to all three.
function AdminHolidaysTab({ ctx }) {
  const [country, setCountry] = useState('all');
  const [year, setYear] = useState('all');
  const [adding, setAdding] = useState(false);
  const [f, setF] = useState({ country: HOLIDAY_COUNTRIES[0], date: todayISO(), endDate: '', name: '', officeClosed: true, bufferDays: 0 });
  const all = (ctx.holidays || []).filter(x => x.active !== false);
  const years = [...new Set(all.map(x => x.date.slice(0, 4)))].sort();
  const shown = all
    .filter(x => (country === 'all' || x.country === country) && (year === 'all' || x.date.startsWith(year)))
    .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : a.country.localeCompare(b.country)));
  const approx = all.filter(x => x.approx).length;
  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3 max-w-3xl">
        Public holidays across the countries LEON buys, makes and ships in. Everyone sees these on the
        Calendar, because a factory shut for Tết or a port closed for Golden Week moves a date whether
        or not you work in that country.
      </p>
      <div className="mb-4 rounded-lg border border-[var(--leon-yellow)]/60 bg-[var(--leon-yellow)]/10 px-3 py-2 text-xs">
        <p className="font-semibold text-[var(--leon-yellow)]">Two things to know about this table</p>
        <p className="text-[var(--leon-black)]/65 mt-0.5">
          It ships <b>with the app</b> rather than coming from a live feed, so it needs extending each
          year. And <b>{approx} of these dates move</b> — the lunar holidays (Tết, Spring Festival) and
          every Islamic one (Eid, marked below) depend on the calendar or the moon sighting and differ
          country to country. <b>Confirm those with the local team before scheduling against them.</b>
        </p>
      </div>
      <div className="flex items-center gap-2 flex-wrap mb-3">
        <Select value={country} onChange={e => setCountry(e.target.value)} className="!w-56 !py-1 !text-xs">
          <option value="all">All countries</option>
          {HOLIDAY_COUNTRIES.map(c => <option key={c}>{c}</option>)}
        </Select>
        <Select value={year} onChange={e => setYear(e.target.value)} className="!w-32 !py-1 !text-xs">
          <option value="all">All years</option>
          {years.map(y => <option key={y}>{y}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45">{shown.length} of {all.length}</span>
        <div className="flex-1" />
        <Button size="sm" onClick={() => setAdding(a => !a)}>{adding ? 'Cancel' : '+ Add Holiday'}</Button>
      </div>
      {adding && (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white p-3 mb-3 grid sm:grid-cols-5 gap-2 items-end">
          <Field label="Country">
            <Select value={f.country} onChange={e => setF({ ...f, country: e.target.value })}>
              {HOLIDAY_COUNTRIES.map(c => <option key={c}>{c}</option>)}
            </Select>
          </Field>
          <Field label="Name"><TextInput value={f.name} onChange={e => setF({ ...f, name: e.target.value })} placeholder="e.g. Factory shutdown" /></Field>
          <Field label="From"><TextInput type="date" value={f.date} onChange={e => setF({ ...f, date: e.target.value })} /></Field>
          <Field label="To" hint="Blank for one day"><TextInput type="date" value={f.endDate} onChange={e => setF({ ...f, endDate: e.target.value })} /></Field>
          <Field label="Extra days" hint="Reserved either side"><TextInput type="number" min="0" value={f.bufferDays} onChange={e => setF({ ...f, bufferDays: e.target.value })} /></Field>
          <label className="flex items-center gap-2 text-xs pb-2 cursor-pointer">
            <input type="checkbox" checked={f.officeClosed} className="w-4 h-4 accent-[var(--leon-brown)]"
              onChange={e => setF({ ...f, officeClosed: e.target.checked })} />Our office is closed
          </label>
          <Button disabled={!f.name.trim()} onClick={() => { ctx.addHoliday({ ...f, endDate: f.endDate || null, bufferDays: Number(f.bufferDays) || 0 }); setF({ ...f, name: '' }); setAdding(false); }}>Add</Button>
        </div>
      )}
      <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto">
        <table className="w-full text-sm" style={{ minWidth: 980 }}>
          <thead>
            <tr className="text-left text-[11px] font-bold text-[var(--leon-black)]/50 uppercase border-b border-[var(--leon-line)]">
              <th className="px-3 py-2" style={{ width: 170 }}>Country</th>
              <th className="px-3 py-2" style={{ minWidth: 240 }}>Holiday</th>
              <th className="px-3 py-2" style={{ width: 140 }}>From</th>
              <th className="px-3 py-2" style={{ width: 140 }}>To</th>
              <th className="px-3 py-2 text-center" style={{ width: 110 }}>Office shut</th>
              <th className="px-3 py-2 text-center" style={{ width: 120 }}>Extra days</th>
              <th className="px-3 py-2 w-10"></th>
            </tr>
          </thead>
          <tbody>
            {shown.map(x => (
              <tr key={x.id} className="border-b border-[var(--leon-line)] last:border-0">
                <td className="px-3 py-1.5 whitespace-nowrap">{HOLIDAY_COUNTRY_FLAGS[x.country]} {x.country}</td>
                <td className="px-3 py-1.5">
                  <TextInput value={x.name} onChange={e => ctx.updateHoliday(x.id, { name: e.target.value })} className="!py-1 !text-sm w-full" />
                  {x.approx && <span className="text-[10px] font-semibold text-[var(--leon-yellow)]">date moves — confirm locally</span>}
                </td>
                <td className="px-3 py-1.5"><TextInput type="date" value={x.date} onChange={e => ctx.updateHoliday(x.id, { date: e.target.value })} className="!py-1 !text-xs" /></td>
                <td className="px-3 py-1.5"><TextInput type="date" value={x.endDate || ''} onChange={e => ctx.updateHoliday(x.id, { endDate: e.target.value || null })} className="!py-1 !text-xs" /></td>
                {/* Whether OUR office in that country actually shuts is a
                    separate fact from the holiday existing — some are worked
                    through. */}
                <td className="px-3 py-1.5 text-center">
                  <input type="checkbox" checked={x.officeClosed !== false} className="w-4 h-4 accent-[var(--leon-brown)]"
                    title="Our office in this country is closed"
                    onChange={e => ctx.updateHoliday(x.id, { officeClosed: e.target.checked })} />
                </td>
                {/* A shutdown rarely costs only the days it covers. */}
                <td className="px-3 py-1.5 text-center">
                  <TextInput type="number" min="0" max="30" value={Number(x.bufferDays) || 0}
                    title="Extra days reserved either side — ramp-down before, ramp-up after"
                    onChange={e => ctx.updateHoliday(x.id, { bufferDays: Number(e.target.value) || 0 })}
                    className="!py-1 !text-xs !w-16 text-center" />
                </td>
                <td className="px-3 py-1.5">
                  <IconBtn title="Remove this holiday" onClick={() => { if (confirm(`Remove "${x.name}"?`)) ctx.removeHoliday(x.id); }}>&#10005;</IconBtn>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// Saving and logging the company default schedule.
//
// Edits below persist the moment they are made — that is how the rest of the
// app behaves and it means a half-finished edit is never lost. What was missing
// is the RECORD of a revision: who changed the company's default schedule, when,
// and what moved. So this is not a draft/commit box; it is a "mark this as
// revision N" stamp, with the diff worked out against the previous stamp.
function LeadTimeRevisionBar({ ctx }) {
  const [note, setNote] = useState('');
  const [open, setOpen] = useState(false);
  const [justSaved, setJustSaved] = useState(null);
  const canEdit = !!(ctx.canManageCollection);
  const pending = ctx.leadTimePendingChanges ? ctx.leadTimePendingChanges() : [];
  const log = ctx.leadTimeRevisions || [];
  const last = log[0];

  function save() {
    const e = ctx.saveLeadTimeRevision(note.trim());
    setNote(''); setJustSaved(e);
  }

  return (
    <div className="border border-[var(--leon-line)] rounded-xl bg-white mb-5 overflow-hidden">
      <div className="flex flex-wrap items-center gap-3 px-4 py-3 bg-[var(--leon-cream)] border-b border-[var(--leon-line)]">
        <div className="min-w-0">
          <div className="text-[12px] font-bold uppercase tracking-wider text-[var(--leon-black)]/65">
            Default schedule — {last ? `Revision ${last.n}` : 'never revised'}
          </div>
          <div className="text-[11px] text-[var(--leon-black)]/55">
            {last
              ? `Saved by ${last.by} on ${fmtDate(last.date)}${last.note ? ' — ' + last.note : ''}`
              : 'These are the values the software shipped with.'}
          </div>
        </div>
        <div className="ml-auto flex items-center gap-2">
          {!last
            ? <Badge tone="neutral">No revision stamped</Badge>
            : pending.length > 0
              ? <Badge tone="amber">{pending.length} change{pending.length === 1 ? '' : 's'} since</Badge>
              : <Badge tone="green">Nothing changed since</Badge>}
          <Button size="sm" variant="outline" onClick={() => setOpen(o => !o)}>
            {open ? 'Hide history' : `History (${log.length})`}
          </Button>
        </div>
      </div>

      <div className="px-4 py-3">
        {pending.length > 0 ? (
          <>
            <div className="text-[12px] font-semibold mb-1.5">
              {last ? `Changed since revision ${last.n}` : 'Nothing has been stamped yet'}
            </div>
            <ul className="text-[12px] text-[var(--leon-black)]/70 space-y-0.5 mb-3 max-h-40 overflow-auto">
              {pending.slice(0, 40).map((c, i) => <li key={i}>· {c}</li>)}
              {pending.length > 40 && <li className="text-[var(--leon-black)]/45">…and {pending.length - 40} more.</li>}
            </ul>
            {canEdit ? (
              <div className="flex flex-wrap items-end gap-2">
                <Field label="What changed, and why" className="flex-1 min-w-[16rem]">
                  <TextInput value={note} onChange={e => setNote(e.target.value)}
                    placeholder="e.g. Casework production up to 25 days after the Q3 review" />
                </Field>
                <Button onClick={save}>Save as revision {last ? last.n + 1 : 1}</Button>
              </div>
            ) : (
              <p className="text-[12px] text-[var(--leon-black)]/50">
                You can read these but not revise them — that follows your LEON Collection permission.
              </p>
            )}
          </>
        ) : (
          <p className="text-[12px] text-[var(--leon-black)]/55">
            Every edit below is saved as you make it. Pressing <b>Save as revision</b> once you are done
            stamps a numbered version with your name and the date, so it is clear when the company&rsquo;s
            default schedule last moved and what moved in it.
          </p>
        )}

        {justSaved && (
          <p className="text-[12px] text-[var(--leon-brown)] font-semibold mt-2">
            Saved as revision {justSaved.n} — {justSaved.changes.length} change{justSaved.changes.length === 1 ? '' : 's'} recorded.
          </p>
        )}

        {open && (
          <div className="mt-3 border-t border-[var(--leon-line)] pt-3">
            {!log.length ? <p className="text-[12px] text-[var(--leon-black)]/45">No revisions saved yet.</p> : (
              <div className="space-y-3 max-h-80 overflow-auto">
                {log.map(r => (
                  <div key={r.id}>
                    <div className="text-[12px] font-semibold">
                      Revision {r.n} · {fmtDate(r.date)} · {r.by}
                      {r.note ? <span className="font-normal text-[var(--leon-black)]/60"> — {r.note}</span> : null}
                    </div>
                    <ul className="text-[11px] text-[var(--leon-black)]/60 mt-0.5 space-y-0.5">
                      {r.changes.length
                        ? r.changes.slice(0, 12).map((c, i) => <li key={i}>· {c}</li>)
                        : <li className="italic">No differences from the previous revision.</li>}
                      {r.changes.length > 12 && <li className="text-[var(--leon-black)]/40">…and {r.changes.length - 12} more.</li>}
                    </ul>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

function AdminLeadTimesTab({ ctx }) {
  // The department chosen in the templates panel governs this whole tab, so
  // the state lives here: the Window System library below is Windows-only and
  // was reading as part of the Interiors setup when it sat there unconditionally.
  const [dept, setDept] = useState(DEPARTMENTS[0]);
  return (
    <div>
      <p className="text-sm text-[var(--leon-black)]/50 mb-4 max-w-3xl">
        Default stage durations copied into a scope the moment it is created. Editing here never
        reschedules a project already underway.
      </p>
      <LeadTimeRevisionBar ctx={ctx} />
      <h2 className="text-lg font-bold mb-1">Complexity Multipliers</h2>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3">
        Every duration below is multiplied by the project&rsquo;s complexity, so one set of stage
        templates covers a builder-grade unit and a high-end residence without keeping two of everything.
      </p>
      <ComplexityLevelsPanel ctx={ctx} />
      <h2 className="text-lg font-bold mb-1 mt-6">Scope Schedule Templates</h2>
      <p className="text-sm text-[var(--leon-black)]/50 mb-3">
        Per department and scope family: which stages a new scope gets, in what order, and how long each
        one takes. Every family carries a separate template for each way the work is sold, because the
        stages genuinely differ &mdash; Labor Only has no submittal or production to track, Supply Only has
        no installation. The project&rsquo;s complexity multiplier still applies on top of these base durations.
      </p>
      <InteriorLeadTimeLibraryPanel ctx={ctx} dept={dept} setDept={setDept} />
      {/* Windows only. On Interiors this is not "further settings" — it is a
          different department's library, and reading it as part of the
          Interiors setup is exactly the confusion to avoid. */}
      {dept === 'Windows' && (
        <>
          <h2 className="text-lg font-bold mb-1 mt-6">Window System Lead-Time Library</h2>
          <p className="text-sm text-[var(--leon-black)]/50 mb-3">Per named window/door system, for the Window Schedule&rsquo;s own stages &mdash; the approval/production graph a window scope runs on, alongside the stage template above.</p>
          <WindowLeadTimeLibraryPanel ctx={ctx} />
        </>
      )}
    </div>
  );
}

// The company profile itself moved to LEON Library -> Company Setup, so this
// tab is now just the demo-scenario tooling it used to sit above.
function AdminCompanyTab({ ctx }) {
  return <DemoScenarioPanel ctx={ctx} />;
}

// Scope schedule templates. This used to be one 18-column matrix of durations
// covering only Interiors families, which could not express what the work
// actually looks like: the stage LIST differs by department, by family and by
// how the scope is sold, not just the number of days in each stage. So it is
// now department → family → variant, and the variant is an editable ordered
// list of stages rather than a fixed set of columns.
// A template is stored only once it is edited (entry.stageTemplates[variant]);
// until then the family follows the built-in template, which is what lets a
// stage added to the catalogue later still reach every untouched family.
// The multipliers themselves. A project stores its complexity by NAME, so a
// rename here migrates every project that used it (updateComplexityLevel) —
// otherwise those projects would quietly fall back to 1x.
function ComplexityLevelsPanel({ ctx }) {
  const [name, setName] = useState('');
  const [mult, setMult] = useState('');
  const levels = (ctx.complexityLevels || []).filter(l => l.active !== false);
  const used = n => ctx.projects.filter(p => p.complexity === n).length;
  function add() {
    const m = Number(mult);
    if (!name.trim() || !(m > 0)) return;
    if (levels.some(l => l.name.toLowerCase() === name.trim().toLowerCase())) { alert('There is already a level with that name.'); return; }
    ctx.addComplexityLevel({ name: name.trim(), multiplier: m });
    setName(''); setMult('');
  }
  return (
    <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden mb-6">
      <table className="w-full text-sm">
        <thead>
          <tr className="text-left text-[11px] font-bold text-[var(--leon-black)]/50 uppercase border-b border-[var(--leon-line)]">
            <th className="px-3 py-2">Level</th>
            <th className="px-3 py-2 text-center" style={{ width: 140 }}>Multiplier</th>
            <th className="px-3 py-2" style={{ width: 200 }}>Example</th>
            <th className="px-3 py-2 text-right" style={{ width: 120 }}>In use</th>
            <th className="px-3 py-2 w-10"></th>
          </tr>
        </thead>
        <tbody>
          {levels.map(l => (
            <tr key={l.id} className="border-b border-[var(--leon-line)] last:border-0">
              <td className="px-3 py-1.5">
                <TextInput defaultValue={l.name} onBlur={e => { const v = e.target.value.trim(); if (v && v !== l.name) ctx.updateComplexityLevel(l.id, { name: v }); }} className="!py-1 !text-sm !w-48" />
              </td>
              <td className="px-3 py-1.5 text-center">
                <div className="flex items-center justify-center gap-1">
                  <TextInput type="number" step="0.05" min="0.1" defaultValue={l.multiplier}
                    onBlur={e => { const v = Number(e.target.value); if (v > 0 && v !== l.multiplier) ctx.updateComplexityLevel(l.id, { multiplier: v }); }}
                    className="!py-1 !text-sm !w-20 text-center" />
                  <span className="text-xs text-[var(--leon-black)]/40">×</span>
                </div>
              </td>
              <td className="px-3 py-1.5 text-xs text-[var(--leon-black)]/45 tabular-nums">
                a 20-day stage &rarr; <b>{Math.round(20 * (Number(l.multiplier) || 1))} d</b>
              </td>
              <td className="px-3 py-1.5 text-right text-xs text-[var(--leon-black)]/50 tabular-nums">
                {used(l.name) ? `${used(l.name)} project${used(l.name) === 1 ? '' : 's'}` : <span className="text-[var(--leon-black)]/25">—</span>}
              </td>
              <td className="px-3 py-1.5">
                {levels.length > 1 && (
                  <IconBtn title={used(l.name) ? 'Retire this level — the projects already on it keep it' : 'Retire this level'}
                    onClick={() => { if (confirm(`Retire "${l.name}"?${used(l.name) ? `\n\n${used(l.name)} project(s) use it and will keep it — it just stops being offered for new ones.` : ''}`)) ctx.removeComplexityLevel(l.id); }}>&#10005;</IconBtn>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="flex items-center gap-2 flex-wrap px-3 py-2 border-t border-[var(--leon-line)] bg-[var(--leon-cream)]/40">
        <TextInput value={name} onChange={e => setName(e.target.value)} placeholder="New level, e.g. Ultra Luxury" className="!py-1 !text-xs !w-52" />
        <TextInput type="number" step="0.05" min="0.1" value={mult} onChange={e => setMult(e.target.value)} placeholder="2.5" className="!py-1 !text-xs !w-24 text-center" />
        <span className="text-xs text-[var(--leon-black)]/40">×</span>
        <Button size="sm" variant="outline" onClick={add} disabled={!name.trim() || !(Number(mult) > 0)}>+ Add level</Button>
        <span className="text-[11px] text-[var(--leon-black)]/40">renaming a level carries every project already on it</span>
      </div>
    </div>
  );
}

function InteriorLeadTimeLibraryPanel({ ctx, dept, setDept }) {
  const entries = ctx.interiorLeadTimeLibrary || [];
  const inDept = entries.filter(e => familyDepartment(e.familyName, ctx.scopeLibrary) === dept);
  const [famName, setFamName] = useState('');
  const entry = inDept.find(e => e.familyName === famName) || inDept[0] || null;
  const variants = entry ? templateVariantsForFamily(entry.familyName, ctx.scopeLibrary) : [];
  const [variant, setVariant] = useState(SCOPE_TYPES[0]);
  const activeVariant = variants.includes(variant) ? variant : variants[0];

  if (!entries.length) return <EmptyState text="No scope families yet." />;

  // What this variant currently is: the saved order if one was edited, else
  // the built-in template. Both come back as full stage defs so the row can
  // show the standard duration alongside the family's own.
  const custom = entry && entry.stageTemplates ? entry.stageTemplates[activeVariant] : null;
  const defs = entry ? stageDefsForScope(entry.familyName, ctx.scopeLibrary, ctx.interiorLeadTimeLibrary, activeVariant) : [];
  const keys = defs.map(d => d.key);
  const missing = ALL_STAGE_DEFS.filter(d => !keys.includes(d.key));
  const total = defs.reduce((n, d) => n + (Number(entry.stageDays[d.key]) || d.baseDays || 0), 0);

  function save(nextKeys) { ctx.setScopeTemplateStages(entry.id, activeVariant, nextKeys); }
  function move(i, dir) {
    const next = [...keys];
    const j = i + dir;
    if (j < 0 || j >= next.length) return;
    [next[i], next[j]] = [next[j], next[i]];
    save(next);
  }

  return (
    <div className="mb-6">
      {/* Department first — the two run different work and share nothing but
          the vocabulary, so mixing them in one list only ever meant scrolling
          past the half you did not want. */}
      <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white w-fit mb-3">
        {DEPARTMENTS.map(d => (
          <button key={d} type="button" onClick={() => { setDept(d); setFamName(''); }}
            className={`px-3 py-1.5 rounded-md text-sm font-semibold ${dept === d ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/60'}`}>
            {d === 'Windows' ? '🪟 Windows' : '🪑 Interiors'}
          </button>
        ))}
      </div>
      {!entry ? <EmptyState text={`No ${dept} scope families yet.`} /> : (
        <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-hidden">
          <div className="flex items-center gap-2 flex-wrap px-3 py-2 bg-[var(--leon-cream)]">
            <span className="text-[11px] uppercase tracking-wide font-bold text-[var(--leon-black)]/50">Family</span>
            <Select value={entry.familyName} onChange={e => setFamName(e.target.value)} className="!py-1 !text-xs !w-56">
              {inDept.map(e => <option key={e.id} value={e.familyName}>{e.familyName}</option>)}
            </Select>
            <div className="flex gap-1 border border-[var(--leon-line)] rounded-lg p-0.5 bg-white">
              {variants.map(v => (
                <button key={v} type="button" onClick={() => setVariant(v)}
                  className={`px-2.5 py-1 rounded-md text-xs font-semibold ${activeVariant === v ? 'bg-[var(--leon-black)] text-white' : 'text-[var(--leon-black)]/55'}`}>
                  {templateVariantLabel(entry.familyName, ctx.scopeLibrary, v)}
                  {entry.stageTemplates && entry.stageTemplates[v] ? ' •' : ''}
                </button>
              ))}
            </div>
            <div className="flex-1" />
            <span className="text-xs text-[var(--leon-black)]/45 tabular-nums">{defs.length} stages · {total} d</span>
            {custom && <Button size="sm" variant="ghost" onClick={() => { if (confirm(`Put ${entry.familyName} — ${templateVariantLabel(entry.familyName, ctx.scopeLibrary, activeVariant)} back to the standard stage list?`)) ctx.resetScopeTemplate(entry.id, activeVariant); }}>Reset to standard</Button>}
          </div>
          {custom && (
            <p className="px-3 pt-2 text-[11px] text-[var(--leon-yellow)] font-semibold">
              Edited — this family uses its own stage list for {templateVariantLabel(entry.familyName, ctx.scopeLibrary, activeVariant)}, not the standard one.
            </p>
          )}
          <table className="w-full text-sm">
            <thead>
              <tr className="text-left text-[11px] font-bold text-[var(--leon-black)]/50 uppercase border-b border-[var(--leon-line)]">
                <th className="px-3 py-2 w-10">#</th>
                <th className="px-3 py-2">Stage</th>
                <th className="px-3 py-2">Owner</th>
                <th className="px-3 py-2 text-center" style={{ width: 110 }}>Days</th>
                <th className="px-3 py-2 text-right" style={{ width: 110 }}>Order</th>
                <th className="px-3 py-2 w-10"></th>
              </tr>
            </thead>
            <tbody>
              {defs.map((d, i) => {
                const std = stageDefByKey(d.key);
                const days = Number(entry.stageDays[d.key]) || (std ? std.baseDays : 0);
                const off = std && days !== std.baseDays;
                return (
                  <tr key={d.key} className="border-b border-[var(--leon-line)] last:border-0">
                    <td className="px-3 py-1.5 text-[var(--leon-black)]/35 tabular-nums">{i + 1}</td>
                    <td className="px-3 py-1.5 font-semibold">{d.name}
                      {d.name !== (std ? std.name : d.name) && <span className="ml-2 text-[10px] text-[var(--leon-black)]/35 font-normal">({std.name})</span>}
                    </td>
                    <td className="px-3 py-1.5 text-xs text-[var(--leon-black)]/50">{d.role}</td>
                    <td className="px-3 py-1.5 text-center">
                      <input type="number" min="0" value={days} title={std ? `Standard is ${std.baseDays} days` : ''}
                        onChange={e => ctx.setInteriorLeadTimeStageDays(entry.id, d.key, e.target.value)}
                        className={`w-16 rounded-md border px-1 py-1 text-sm text-center bg-white focus:border-[var(--leon-brown)] ${off ? 'border-[var(--leon-brown)] text-[var(--leon-brown)] font-semibold' : 'border-[var(--leon-line)]'}`} />
                    </td>
                    <td className="px-3 py-1.5 text-right whitespace-nowrap">
                      <IconBtn title="Move earlier" onClick={() => move(i, -1)}>&#8593;</IconBtn>
                      <IconBtn title="Move later" onClick={() => move(i, 1)}>&#8595;</IconBtn>
                    </td>
                    <td className="px-3 py-1.5">
                      <IconBtn title={`Remove ${d.name} from this template`} onClick={() => { if (confirm(`Remove "${d.name}" from ${entry.familyName} — ${templateVariantLabel(entry.familyName, ctx.scopeLibrary, activeVariant)}?\n\nScopes already created keep the stage; this only changes what new ones get.`)) save(keys.filter(k => k !== d.key)); }}>&#10005;</IconBtn>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          <div className="flex items-center gap-2 flex-wrap px-3 py-2 border-t border-[var(--leon-line)] bg-[var(--leon-cream)]/40">
            <span className="text-xs text-[var(--leon-black)]/50">Add a stage</span>
            <Select value="" onChange={e => { if (e.target.value) save([...keys, e.target.value]); }} className="!py-1 !text-xs !w-64">
              <option value="">— choose a stage —</option>
              {missing.map(d => <option key={d.key} value={d.key}>{d.name}</option>)}
            </Select>
            <span className="text-[11px] text-[var(--leon-black)]/40">it lands at the end — use ↑ to place it</span>
          </div>
        </div>
      )}
      <p className="text-[11px] text-[var(--leon-black)]/40 mt-2">
        Durations are shared across a family&rsquo;s variants &mdash; a Casework production run takes the same
        time whoever installs it. Only the stage list is per variant. Highlighted days differ from the
        standard for that stage. <b>Nothing here reschedules work already underway</b>: a template is copied
        into a scope when the scope is created, so editing it only changes what new scopes get.
      </p>
    </div>
  );
}

function WindowLeadTimeLibraryPanel({ ctx }) {
  const [showAdd, setShowAdd] = useState(false);
  const FIELDS = [
    { key: 'profileApprovalDays', label: 'Profile/System Approval' },
    { key: 'glassLeadDays', label: 'Glass Lead Time' },
    { key: 'fabDrawingApprovalDays', label: 'Fab Drawing Approval' },
    { key: 'factoryFirstDeliveryDays', label: 'Factory: First Delivery' },
    { key: 'factoryFullProductionDays', label: 'Factory: Full Production' },
    { key: 'shippingDays', label: 'Shipping' },
  ];
  return (
    <div className="border border-[var(--leon-line)] rounded-xl bg-white overflow-x-auto mb-6">
      <table className="w-full text-sm" style={{ minWidth: 760 }}>
        <thead>
          <tr className="border-b border-[var(--leon-line)] text-left text-[11px] font-bold text-[var(--leon-black)]/50 uppercase">
            <th className="px-3 py-2">System</th>
            {FIELDS.map(f => <th key={f.key} className="px-2 py-2 text-right whitespace-nowrap">{f.label}</th>)}
            <th className="px-3 py-2"></th>
          </tr>
        </thead>
        <tbody>
          {ctx.windowLeadTimeLibrary.map(entry => (
            <tr key={entry.id} className={`border-b border-[var(--leon-line)] last:border-0 ${entry.active ? '' : 'opacity-40'}`}>
              <td className="px-3 py-1.5 font-semibold whitespace-nowrap">{entry.name}</td>
              {FIELDS.map(f => (
                <td key={f.key} className="px-2 py-1.5">
                  <input type="number" min="0" value={entry[f.key]} disabled={!entry.active}
                    onChange={e => ctx.updateWindowLeadTimeEntry(entry.id, { [f.key]: Number(e.target.value) || 0 })}
                    className="w-16 rounded-md border border-[var(--leon-line)] px-2 py-1 text-sm text-right bg-white focus:border-[var(--leon-brown)] disabled:bg-[var(--leon-cream)]" />
                </td>
              ))}
              <td className="px-3 py-1.5 text-right whitespace-nowrap">
                <Button size="sm" variant={entry.active ? 'outline' : 'primary'} onClick={() => ctx.setWindowLeadTimeEntryActive(entry.id, !entry.active)}>{entry.active ? 'Retire' : 'Activate'}</Button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="p-2.5 border-t border-[var(--leon-line)]">
        {showAdd
          ? <AddWindowLeadTimeEntryForm ctx={ctx} fields={FIELDS} onDone={() => setShowAdd(false)} />
          : <Button size="sm" variant="ghost" onClick={() => setShowAdd(true)}>+ Add System</Button>}
      </div>
    </div>
  );
}

function AddWindowLeadTimeEntryForm({ ctx, fields, onDone }) {
  const [name, setName] = useState('');
  const [vals, setVals] = useState(() => Object.fromEntries(fields.map(f => [f.key, 0])));
  return (
    <div className="flex items-end flex-wrap gap-2">
      <Field label="System Name"><TextInput autoFocus value={name} onChange={e => setName(e.target.value)} className="!w-44" /></Field>
      {fields.map(f => (
        <Field key={f.key} label={f.label}>
          <input type="number" min="0" value={vals[f.key]} onChange={e => setVals(v => ({ ...v, [f.key]: Number(e.target.value) || 0 }))}
            className="w-16 rounded-md border border-[var(--leon-line)] px-2 py-1.5 text-sm text-right bg-white focus:border-[var(--leon-brown)]" />
        </Field>
      ))}
      <Button size="sm" onClick={() => { if (name.trim()) { ctx.addWindowLeadTimeEntry({ name: name.trim(), ...vals }); onDone(); } }}>Save</Button>
      <Button size="sm" variant="ghost" onClick={onDone}>Cancel</Button>
    </div>
  );
}

function FamilyEditor({ ctx, fam, isFirst, isLast }) {
  const [rename, setRename] = useState(false);
  const [nameVal, setNameVal] = useState(fam.name);
  const [newCat, setNewCat] = useState('');
  return (
    <Collapsible
      title={rename ? '' : fam.name}
      right={
        <div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
          {rename && <><TextInput autoFocus value={nameVal} onChange={e => setNameVal(e.target.value)} className="!py-1 !text-xs !w-40" /><Button size="sm" onClick={() => { ctx.lib.renameFamily(fam.id, nameVal); setRename(false); }}>Save</Button></>}
          <IconBtn title="Rename" onClick={() => setRename(r => !r)}>✎</IconBtn>
          <IconBtn title="Move up" onClick={() => ctx.lib.moveFamily(fam.id, -1)} className={isFirst ? 'opacity-30 pointer-events-none' : ''}>↑</IconBtn>
          <IconBtn title="Move down" onClick={() => ctx.lib.moveFamily(fam.id, 1)} className={isLast ? 'opacity-30 pointer-events-none' : ''}>↓</IconBtn>
          <Button size="sm" variant={fam.isWindowSystem ? 'primary' : 'outline'} title="Use the specialized Window/Exterior Door System schedule template for scopes in this family" onClick={() => ctx.lib.toggleFamilyWindowFlag(fam.id)}>{fam.isWindowSystem ? '🪟 Window Schedule' : 'Use Window Schedule'}</Button>
          <Button size="sm" variant={fam.isCountertopSystem ? 'primary' : 'outline'} title="Countertops are always supply AND labour, on their own fabrication-shop schedule: submittal, PI, production, shipping to the fabrication shop, template, fabrication, then install." onClick={() => ctx.lib.toggleFamilyCountertopFlag(fam.id)}>{fam.isCountertopSystem ? '🪨 Countertop Schedule' : 'Use Countertop Schedule'}</Button>
          <Button size="sm" variant={fam.active ? 'outline' : 'primary'} onClick={() => ctx.lib.toggleFamily(fam.id)}>{fam.active ? 'Deactivate' : 'Activate'}</Button>
        </div>
      }
      count={fam.categories.length}
    >
      {fam.categories.map((cat, ci) => (
        <CategoryEditor key={cat.id} ctx={ctx} fam={fam} cat={cat} isFirst={ci === 0} isLast={ci === fam.categories.length - 1} />
      ))}
      <div className="flex items-center gap-2 mt-2">
        <TextInput value={newCat} onChange={e => setNewCat(e.target.value)} placeholder="New selection category" className="!w-56 !py-1 !text-xs" />
        <Button size="sm" onClick={() => { if (newCat.trim()) { ctx.lib.addCategory(fam.id, newCat.trim()); setNewCat(''); } }}>+ Add Category</Button>
      </div>
    </Collapsible>
  );
}

function CategoryEditor({ ctx, fam, cat, isFirst, isLast }) {
  const [rename, setRename] = useState(false);
  const [nameVal, setNameVal] = useState(cat.name);
  const [newOpt, setNewOpt] = useState('');
  return (
    <div className="border border-[var(--leon-line)] rounded-lg p-2.5 mb-2 bg-[var(--leon-cream)]/40">
      <div className="flex items-center justify-between gap-2 mb-1.5">
        {rename ? (
          <div className="flex items-center gap-1"><TextInput autoFocus value={nameVal} onChange={e => setNameVal(e.target.value)} className="!py-1 !text-xs !w-40" /><Button size="sm" onClick={() => { ctx.lib.renameCategory(cat.id, nameVal); setRename(false); }}>Save</Button></div>
        ) : <span className="text-sm font-bold">{cat.name} {!cat.active && <Badge tone="neutral">Inactive</Badge>}</span>}
        <div className="flex items-center gap-1">
          <IconBtn title="Rename" onClick={() => setRename(r => !r)}>✎</IconBtn>
          <IconBtn title="Move up" onClick={() => ctx.lib.moveCategory(fam.id, cat.id, -1)} className={isFirst ? 'opacity-30 pointer-events-none' : ''}>↑</IconBtn>
          <IconBtn title="Move down" onClick={() => ctx.lib.moveCategory(fam.id, cat.id, 1)} className={isLast ? 'opacity-30 pointer-events-none' : ''}>↓</IconBtn>
          <Button size="sm" variant={cat.active ? 'outline' : 'primary'} onClick={() => ctx.lib.toggleCategory(cat.id)}>{cat.active ? 'Deactivate' : 'Activate'}</Button>
        </div>
      </div>
      {/* Options render as picture cards, matching Supplier Finishes — a finish
          is chosen by eye, so a 20px chip was the wrong unit. */}
      <div className="grid gap-2 mb-2 [grid-template-columns:repeat(auto-fill,minmax(132px,1fr))]">
        {cat.options.map((opt, oi) => <OptionCard key={opt.id} ctx={ctx} cat={cat} opt={opt} isFirst={oi === 0} isLast={oi === cat.options.length - 1} />)}
      </div>
      <div className="flex items-center gap-2">
        <TextInput value={newOpt} onChange={e => setNewOpt(e.target.value)} placeholder="New finish option" className="!w-48 !py-1 !text-xs" />
        <Button size="sm" variant="ghost" onClick={() => { if (newOpt.trim()) { ctx.lib.addOption(cat.id, newOpt.trim()); setNewOpt(''); } }}>+ Add Option</Button>
      </div>
    </div>
  );
}

function OptionCard({ ctx, cat, opt, isFirst, isLast }) {
  const [rename, setRename] = useState(false);
  const [nameVal, setNameVal] = useState(opt.name);
  const [viewing, setViewing] = useState(false);
  return (
    <div className={`border rounded-lg overflow-hidden ${opt.active ? 'bg-white border-[var(--leon-line)]' : 'bg-[var(--leon-cream)] border-[var(--leon-line)] opacity-60'}`}>
      {/* Click the picture to view it large; the ImagePicker overlay stays for
          setting or replacing the image. */}
      <div className="relative group">
        {opt.imageUrl ? (
          <button type="button" onClick={() => setViewing(true)} title="Click to view the picture" className="block w-full cursor-zoom-in">
            <img src={opt.imageUrl} alt="" loading="lazy" className="w-full h-20 object-cover" />
          </button>
        ) : (
          <div className="w-full h-20 bg-[var(--leon-cream)] flex items-center justify-center text-[10px] text-[var(--leon-black)]/30">No image</div>
        )}
        <div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition">
          <ImagePicker url={opt.imageUrl} onChange={url => ctx.lib.setOptionImage(opt.id, url)} size={22} />
        </div>
      </div>
      <div className="p-1.5">
        {rename ? (
          <div className="flex items-center gap-1">
            <TextInput autoFocus value={nameVal} onChange={e => setNameVal(e.target.value)} className="!py-0.5 !text-xs" />
            <IconBtn title="Save" onClick={() => { ctx.lib.renameOption(opt.id, nameVal); setRename(false); }}>✓</IconBtn>
          </div>
        ) : (
          <div className="text-xs font-semibold truncate" title={opt.name}>{opt.name}</div>
        )}
        {!rename && (
          <div className="flex items-center gap-0.5 mt-0.5">
            <IconBtn title="Rename" onClick={() => setRename(true)} className="!w-5 !h-5">✎</IconBtn>
            <IconBtn title="Move left" onClick={() => ctx.lib.moveOption(cat.id, opt.id, -1)} className={`!w-5 !h-5 ${isFirst ? 'opacity-30 pointer-events-none' : ''}`}>←</IconBtn>
            <IconBtn title="Move right" onClick={() => ctx.lib.moveOption(cat.id, opt.id, 1)} className={`!w-5 !h-5 ${isLast ? 'opacity-30 pointer-events-none' : ''}`}>→</IconBtn>
            <div className="flex-1" />
            <button onClick={() => ctx.lib.toggleOption(opt.id)}
              className={`text-[10px] font-semibold ${opt.active ? 'text-[var(--leon-red)]' : 'text-[var(--leon-brown)]'}`}>
              {opt.active ? 'Retire' : 'Activate'}
            </button>
          </div>
        )}
      </div>
      {viewing && <SupplierFinishViewer rec={{ name: opt.name, img: opt.imageUrl, cat: cat.name }} onClose={() => setViewing(false)} />}
    </div>
  );
}

// ============================================================================
// Mount
// ============================================================================
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
