/* editor.jsx — the shared, full-fidelity invoice editor.
 * EditorBody is the single source of truth for the editable draft; it is used
 * BOTH by the AI composer (under the chat) and by the first-class Manual editor,
 * so an invoice started by AI and one built by hand converge on identical UI.
 * → window.QEditor.{EditorBody, draftFromInvoice}; window.QScreens.ManualEditor
 */
(function () {
  const { useState, useRef, useEffect } = React;
  const Q = window.Q, LIB = window.LIB, Icons = window.Icons;
  const { Icon } = Icons;
  const { Button, IconButton, Sparkle, Badge, Money, MoneyDisplay, Kicker, AiKicker, Card, Field, Input, Textarea, Select, Avatar, Sheet, EmptyState, Segmented, ReceiptThumb, ReceiptViewer } = Q;
  const t = window.t;
  /* t() with a literal fallback: shows the fallback if the key is undefined
   * (t returns the raw key in that case) so micro-labels never leak a key. */
  const tx = (key, fallback, vars) => { const s = t(key, vars); return s === key ? fallback : s; };

  /* Kill the native number-input spinners (the up/down arrows) inside the
     editor. They clutter the dense line rows and let a stray scroll/long-press
     nudge a money figure. Injected once; pairs with NumCell's onWheel blur. */
  if (typeof document !== "undefined" && !document.getElementById("q-editor-numcell")) {
    const s = document.createElement("style");
    s.id = "q-editor-numcell";
    s.textContent = ".q-numcell::-webkit-outer-spin-button,.q-numcell::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}.q-numcell{-moz-appearance:textfield;appearance:textfield;}";
    document.head.appendChild(s);
  }

  let _uid = 5000;
  const uid = () => "x" + (++_uid);
  const newClientId = () => "c" + Date.now().toString(36) + Math.floor(Math.random() * 1e3);
  const blankClient = (currency) => ({ id: newClientId(), name: "", contact: "", email: "", phone: "", address: "", vat: "", reg: "", sector: "", currency, projects: [] });
  const addDays = (iso, n) => { const d = new Date(iso); d.setDate(d.getDate() + n); return d.toISOString(); };

  /* Unit labels resolved through t() at render time so they follow the
     active locale (FR/EN); falls back to the raw value if a key is missing. */
  const unitOpts = () => LIB.UNITS.map(u => { const k = "unit." + u.value; const s = t(k); return { value: u.value, label: s === k ? u.label : s }; });

  /* Notes prefill: the business default from Settings when set, else the
     localized stock line (contract: COMPANY.notesDefault may be ""). */
  const defaultNotes = (co, days) => {
    const s = (co && co.notesDefault ? String(co.notesDefault) : "").trim();
    return s ? s.replace(/\{days\}/g, days) : t("inv.notes.default", { days });
  };

  /* Convert a stored invoice into a working draft the editor can mutate.
   * Items go through LIB.cleanItem so provenance (source / catalogId /
   * expenseId / memberId / receipt) survives the round-trip; the audit
   * trail is carried so an edit-save never loses history. */
  function draftFromInvoice(inv) {
    return {
      editId: inv.id, kind: inv.kind || "invoice", clientId: inv.clientId,
      newClientName: inv.newClientName || null, projectId: inv.projectId || null,
      issued: inv.issued, terms: LIB.daysBetween(inv.issued, inv.due), due: inv.due,
      currency: inv.currency, status: inv.status || "draft", vatMode: inv.vatMode || "standard",
      items: inv.items.map(it => LIB.cleanItem(it, inv)),
      discountPct: inv.discountPct || 0, taxRate: inv.taxRate || 0, taxName: inv.taxName || "", notes: inv.notes || "", legal: inv.legal || "", paid: inv.paid || 0,
      recurrence: inv.recurrence || null,
      quoteId: inv.quoteId || null, depositOf: inv.depositOf || null,
      depositPct: inv.depositPct != null ? inv.depositPct : null, creditFor: inv.creditFor || null,
      audit: inv.audit || [],
    };
  }

  /* Flip the expenses behind newly-saved expense lines to "billed" and log
   * an audit event per expense on the invoice. Quotes and credit notes
   * never bill expenses.
   * Returns { working, inv } (both possibly replaced, never mutated). */
  function billExpenses(working, inv) {
    if (inv.kind !== "invoice") return { working, inv };
    const ids = (inv.items || []).filter(it => it.source === "expense" && it.expenseId).map(it => it.expenseId);
    if (!ids.length) return { working, inv };
    let out = inv;
    const EXPENSES = (working.EXPENSES || []).map(e => {
      if (ids.indexOf(e.id) === -1 || e.status === "billed") return e;
      out = LIB.pushAudit(out, LIB.auditEvent("expense", { merchant: e.merchant, amount: window.fmtMoney(e.amount, e.currency || inv.currency) }, e.receipt || null));
      return { ...e, status: "billed", invoiceId: inv.id };
    });
    return { working: { ...working, EXPENSES }, inv: out };
  }

  /* ── A single editable line item ─────────────────────────────── */
  function LineItem({ it, currency, onChange, onRemove, onMoveUp, onMoveDown, canUp, canDown, meta, onViewReceipt, taxes, onAddTax, onTaxPicker, vatLocked }) {
    const [open, setOpen] = useState(false);
    const gross = LIB.lineTotal(it);
    const net = LIB.lineNet(it);
    const hasDisc = (Number(it.discountPct) || 0) > 0;
    return (
      <div style={{ padding: "12px 0", borderBottom: "1px solid var(--color-border)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <input value={it.description} onChange={e => onChange({ ...it, description: e.target.value })} placeholder={t("inv.item")}
            style={{ flex: 1, minWidth: 0, border: "none", background: "transparent", fontSize: 14, fontWeight: 600, color: "var(--color-text-1)", fontFamily: "var(--font-sans)", padding: 0, textOverflow: "ellipsis" }} />
          <button onClick={() => setOpen(o => !o)} aria-label={t("man.linedetail")} className="q-tap" style={{ border: "none", background: open ? "var(--color-surface-2)" : "transparent", color: open ? "var(--color-text-1)" : "var(--color-text-3)", cursor: "pointer", padding: 9, margin: -4, display: "flex", borderRadius: 8 }}>
            <Icon name="sliders" size={15} />
          </button>
          <button onClick={onRemove} aria-label={t("g.remove")} style={{ border: "none", background: "transparent", color: "var(--color-text-3)", cursor: "pointer", padding: 9, margin: -4, display: "flex" }}>
            <Icon name="x" size={16} />
          </button>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 8 }}>
          <NumCell value={it.qty} onChange={v => onChange({ ...it, qty: v })} width={46} aria={t("inv.qty")} shrink minW={34} />
          <div style={{ width: 78, minWidth: 52, flexShrink: 1 }}>
            <Select value={it.unit || "unit"} onChange={e => onChange({ ...it, unit: e.target.value })} options={unitOpts()}
              style={{ height: 40, fontSize: 13, paddingLeft: 10, paddingRight: 26 }} />
          </div>
          <span style={{ color: "var(--color-text-3)", fontFamily: "var(--font-mono)", fontSize: 13, flexShrink: 0 }}>×</span>
          <NumCell value={it.rate} onChange={v => onChange({ ...it, rate: v })} width={78} money aria={t("inv.rate")} shrink minW={52} />
          <div style={{ flex: 1, minWidth: 0, textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", overflow: "hidden" }}>
            <div style={{ maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis" }}>
              <Money amount={net} currency={currency} size={14} />
            </div>
            {hasDisc && <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--color-text-3)", textDecoration: "line-through", maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis" }}>{window.fmtMoney(gross, currency, { cents: false })}</span>}
          </div>
        </div>
        {/* provenance: where this line came from (catalog / expense / team) */}
        {meta && (
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 7, minWidth: 0 }}>
            <Icon name={meta.icon} size={12} color="var(--color-text-3)" style={{ flexShrink: 0 }} />
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{meta.label}</span>
            {meta.warn && (
              <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, color: meta.warn.color, whiteSpace: "nowrap", flexShrink: 0 }}>
                <span style={{ width: 5, height: 5, borderRadius: "50%", background: "currentColor" }} />{meta.warn.text}
              </span>
            )}
            {meta.receipt && <ReceiptThumb receipt={meta.receipt} size={22} onClick={onViewReceipt ? () => onViewReceipt(meta.receipt) : undefined} style={{ marginLeft: "auto" }} />}
          </div>
        )}
        {/* per-line detail */}
        {open && (
          <div className="q-fade" style={{ marginTop: 12, padding: 12, background: "var(--color-surface-1)", borderRadius: 8, display: "flex", flexDirection: "column", gap: 12 }}>
            <div style={{ display: "flex", gap: 12 }}>
              <div style={{ flex: 1 }}>
                <Kicker style={{ marginBottom: 6 }}>{t("inv.linedisc")} %</Kicker>
                <NumCell value={it.discountPct} onChange={v => onChange({ ...it, discountPct: v })} width="100%" suffix="%" />
              </div>
              <div style={{ flex: 1.4 }}>
                <Kicker style={{ marginBottom: 6 }}>{t("inv.vat")} %</Kicker>
                <TaxField value={{ rate: it.taxRate, name: it.taxName }} taxes={taxes} onAddTax={onAddTax} onToggle={onTaxPicker} disabled={vatLocked}
                  onChange={({ rate, name }) => onChange({ ...it, taxRate: rate, taxName: name })} />
              </div>
            </div>
            <div style={{ display: "flex", gap: 8, paddingTop: 2 }}>
              <DetailBtn icon="arrowUp" label={tx("man.moveup", "Move up")} onClick={onMoveUp} disabled={!canUp} />
              <DetailBtn icon="arrowDown" label={tx("man.movedown", "Move down")} onClick={onMoveDown} disabled={!canDown} />
              <DetailBtn icon="trash" label={t("g.remove")} onClick={onRemove} danger />
            </div>
          </div>
        )}
      </div>
    );
  }
  function DetailBtn({ icon, label, onClick, disabled, danger }) {
    return (
      <button onClick={onClick} disabled={disabled} style={{
        flex: 1, height: 34, borderRadius: 8, cursor: disabled ? "default" : "pointer", opacity: disabled ? 0.35 : 1,
        border: "1.4px solid var(--color-border-strong)", background: "var(--color-surface-0)",
        color: danger ? "var(--color-error)" : "var(--color-text-2)", fontSize: 12, fontWeight: 600,
        display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 4,
      }}><Icon name={icon} size={14} />{label}</button>
    );
  }
  /* ── TaxField + TaxPickerSheet — bounded VAT picker (never overflows) ──
   * Replaces the per-line chip row and the bulk Segmented. A full-width 40px
   * trigger opens a bottom sheet of radio rows (one per configured tax) plus a
   * persistent custom-rate block, so N taxes and a typed rate both fit. */
  function TaxField({ value, onChange, onAddTax, taxes, onToggle, disabled }) {
    const [open, setOpenRaw] = useState(false);
    const setOpen = (v) => { setOpenRaw(v); if (onToggle) onToggle(v); };
    const rate = value && value.rate != null ? value.rate : 0;
    const name = (value && value.name) || "";
    /* Under reverse-charge / exempt the rate is overridden to 0 everywhere
       (LIB.totals), so editing VAT% is a dead control: show N/A and lock it. */
    const label = disabled ? tx("inv.vat.na", "N/A") : (name ? `${name} (${rate}%)` : LIB.fmtPct(rate));
    return (
      <React.Fragment>
        <button type="button" disabled={disabled} aria-disabled={disabled || undefined} onClick={() => { if (!disabled) setOpen(true); }} aria-label={t("inv.vat.pick")} className="q-tap"
          onFocus={e => { if (!disabled) e.currentTarget.style.borderColor = "var(--color-primary)"; }}
          onBlur={e => e.currentTarget.style.borderColor = "var(--color-border-strong)"}
          style={{ width: "100%", height: 40, display: "flex", alignItems: "center", gap: 8, padding: "0 12px", border: "1.4px solid var(--color-border-strong)", borderRadius: "var(--radius-md)", background: disabled ? "var(--color-surface-1)" : "var(--color-surface-0)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.55 : 1, boxSizing: "border-box" }}>
          <span style={{ flex: 1, textAlign: "left", fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 600, color: disabled ? "var(--color-text-3)" : "var(--color-text-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{label}</span>
          <Icon name="chevronDown" size={15} color="var(--color-text-3)" />
        </button>
        <TaxPickerSheet open={open} onClose={() => setOpen(false)} value={value} onChange={onChange} onAddTax={onAddTax} taxes={taxes || []} />
      </React.Fragment>
    );
  }
  function TaxPickerSheet({ open, onClose, value, onChange, onAddTax, taxes }) {
    const [custom, setCustom] = useState("");
    useEffect(() => { if (open) setCustom(""); }, [open]);
    const curRate = value && value.rate != null ? value.rate : 0;
    const curName = (value && value.name) || "";
    const pick = (rate, name) => { onChange({ rate, name }); onClose(); };
    const applyCustom = () => {
      let r = Number(custom);
      if (!isFinite(r) || r < 0 || r > 100) return;
      r = Math.round(r * 100) / 100; // sanitize: 2-decimal cap, reject >100
      /* Apply to the draft only. The custom rate is NOT pushed to global
         config here (that pollutes Settings unrounded + fires a toast over
         this sheet); save() persists any surviving rate via ensureConfigTax. */
      onChange({ rate: r, name: "" });
      onClose();
    };
    /* Suppress the synthetic "No tax (0%)" row when the configured taxes
       already include a 0% rate — otherwise it duplicates VAT (0%). */
    const hasZeroTax = (taxes || []).some(tx => Number(tx.rate) === 0);
    const rows = hasZeroTax ? [...taxes] : [...taxes, { id: "tx-none", name: "", rate: 0, _none: true }];
    return (
      <Sheet open={open} onClose={onClose} height="auto" title={t("inv.vat.pick")}>
        <div style={{ padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 14 }}>
          <div style={{ border: "1.4px solid var(--color-border-strong)", borderRadius: 12, overflow: "hidden" }}>
            {rows.map((tax, i) => {
              const active = tax._none
                ? (curRate === 0 && !curName)
                : (Number(tax.rate) === 0
                  /* the lone 0% row stands in for "no tax" too, so a 0/"" draft
                     still highlights after the synthetic No-tax row is dropped */
                  ? curRate === 0
                  : (curRate === tax.rate && (curName || "") === (tax.name || "")));
              const label = tax._none ? t("inv.vat.none") : (tax.name ? `${tax.name} (${tax.rate}%)` : LIB.fmtPct(tax.rate));
              return (
                <button key={tax.id || (tax.name + "_" + tax.rate + "_" + i)} type="button" onClick={() => pick(tax.rate, tax.name || "")}
                  className="q-row q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 10, minHeight: 44, padding: "10px 16px", border: "none", borderBottom: i < rows.length - 1 ? "1px solid var(--color-border)" : "none", background: "var(--color-surface-0)", cursor: "pointer" }}>
                  <span style={{ flex: 1, fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 600, color: active ? "var(--color-text-1)" : "var(--color-text-2)" }}>{label}</span>
                  {active && <Icon name="check" size={17} color="var(--color-primary)" />}
                </button>
              );
            })}
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <Kicker>{t("inv.vat.custom")}</Kicker>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <div style={{ flex: 1 }}>
                <NumCell value={custom === "" ? "" : custom} onChange={v => setCustom(v)} width="100%" suffix="%" aria={t("inv.vat.custom")} />
              </div>
              <Button variant="secondary" size="md" onClick={applyCustom} disabled={custom === "" || !isFinite(Number(custom)) || Number(custom) < 0 || Number(custom) > 100}>{t("inv.vat.custom.apply")}</Button>
            </div>
          </div>
        </div>
      </Sheet>
    );
  }
  function NumCell({ value, onChange, width, money, suffix, aria, shrink, minW }) {
    return (
      <div style={{ position: "relative", width, ...(shrink ? { flexShrink: 1, minWidth: minW != null ? minW : 0 } : {}) }}>
        <input type="number" inputMode="decimal" aria-label={aria} className="q-numcell" value={value ?? ""} onChange={e => onChange(e.target.value === "" ? "" : Number(e.target.value))}
          onWheel={e => e.currentTarget.blur()}
          onFocus={e => e.target.style.borderColor = "var(--color-primary)"}
          onBlur={e => e.target.style.borderColor = "var(--color-border-strong)"}
          style={{ width: "100%", height: 40, textAlign: suffix ? "left" : "center", paddingLeft: suffix ? 10 : 4, paddingRight: suffix ? 22 : 4, border: "1.4px solid var(--color-border-strong)", borderRadius: "var(--radius-md)", fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 600, color: money ? "var(--color-gold)" : "var(--color-text-1)", background: "var(--color-surface-0)", outline: "none", fontVariantNumeric: "tabular-nums", boxSizing: "border-box", MozAppearance: "textfield", appearance: "textfield" }} />
        {suffix && <span style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", pointerEvents: "none" }}>{suffix}</span>}
      </div>
    );
  }

  /* ── EditorBody — the shared rich form ───────────────────────── */
  function EditorBody({ draft, setDraft, db, onPickClient, onPickProject, onAddCatalog, onAddExpense, onViewReceipt, headerRight, onAddTax, onTaxPicker }) {
    const tot = LIB.totals(draft);
    const bd = LIB.businessDefaults(db.COMPANY);
    const client = draft.clientId ? LIB.getClient(db, draft.clientId) : null;
    const taxes = (db.config && db.config.taxes) || (db.COMPANY && db.COMPANY.taxes) || [
      { id: "tx-vat-0", name: "VAT", rate: 0 },
      { id: "tx-vat-5.5", name: "VAT", rate: 5.5 },
      { id: "tx-vat-10", name: "VAT", rate: 10 },
      { id: "tx-vat-20", name: "VAT", rate: 20 }
    ];
    /* Nita anticipation (G6: sage + sparkle): an EU cross-border buyer whose
       VAT prefix differs from the seller's usually means reverse charge. */
    const [rcDismissed, setRcDismissed] = useState(false);
    const sellerVat = (db.COMPANY.vat || "").trim().slice(0, 2).toUpperCase();
    const buyerVat = (client && client.vat ? client.vat : "").trim().slice(0, 2).toUpperCase();
    const suggestReverse = !rcDismissed && (draft.vatMode || "standard") === "standard"
      && sellerVat && buyerVat && /^[A-Z]{2}$/.test(sellerVat) && /^[A-Z]{2}$/.test(buyerVat) && sellerVat !== buyerVat;
    /* Under reverse-charge / exempt, LIB.totals overrides every rate to 0, so
       the VAT% picker (bulk + per-line) is a dead control — lock + N/A it. */
    const vatLocked = (draft.vatMode === "reverse" || draft.vatMode === "exempt");
    const project = client && draft.projectId ? (client.projects || []).find(p => p.id === draft.projectId) : null;
    const curOpts = Object.values(LIB.currencyMap(db)).map(c => ({ value: c.code, label: c.code }));
    const up = patch => setDraft({ ...draft, ...patch });

    const setItem = (id, ni) => up({ items: draft.items.map(x => x.id === id ? ni : x) });
    const removeItem = id => up({ items: draft.items.filter(x => x.id !== id) });
    const move = (i, dir) => {
      const arr = draft.items.slice();
      const j = i + dir; if (j < 0 || j >= arr.length) return;
      [arr[i], arr[j]] = [arr[j], arr[i]]; up({ items: arr });
    };
    const addLine = () => up({ items: [...draft.items, { id: uid(), description: "", qty: 1, rate: 0, unit: bd.defaultUnit, discountPct: 0, taxRate: draft.taxRate || 0, taxName: draft.taxName || "" }] });
    /* Bulk VAT value for the TaxField: the rate+name shared by every line
       (so it reflects a custom typed rate too), else the draft-level default. */
    const bulkTaxValue = (() => {
      const items = draft.items;
      if (items.length && items.every(x => x.taxRate === items[0].taxRate && (x.taxName || "") === (items[0].taxName || ""))) {
        return { rate: items[0].taxRate || 0, name: items[0].taxName || "" };
      }
      return { rate: draft.taxRate || 0, name: draft.taxName || "" };
    })();

    /* keep only lines the user actually filled (drops the blank starter row
       when a prefill arrives, never a line with content) */
    const realItems = () => draft.items.filter(x => (x.description && String(x.description).trim()) || Number(x.rate) > 0);
    /* one line per team member, each at their own rate (LIB contract) */
    const teamPending = project && (project.team || []).length > 0 && !draft.items.some(x => x.source === "project");
    const applyTeam = () => {
      const lines = LIB.projectTeamLines(project, draft.taxRate);
      up({ items: [...realItems(), ...lines] });
      Q.toast(t("man.project.teamApplied", { count: lines.length }), "users");
    };
    /* unbilled billable expenses for the picked client (Nita anticipation) */
    const billableExp = client ? (db.EXPENSES || []).filter(e => e.billable && e.status === "unbilled" && e.clientId === client.id) : [];
    const suggestExpenses = !!onAddExpense && billableExp.length > 0 && !draft.items.some(x => x.source === "expense");

    /* provenance meta for a line (label + stock warnings + receipt thumb) */
    const lineMeta = (it) => {
      if (!it.source || it.source === "manual") return null;
      if (it.source === "expense") return { icon: "receipt", label: t("inv.line.from.expense"), receipt: it.receipt || null };
      if (it.source === "project") return { icon: "users", label: t("inv.line.from.project") };
      if (it.source === "catalog") {
        const ci = (db.CATALOG || []).find(c => c.id === it.catalogId);
        const st = ci ? LIB.stockState(ci) : "none";
        const warn = st === "out" ? { text: t("inv.line.outstock"), color: "var(--color-error)" }
          : st === "low" ? { text: t("inv.line.lowstock", { n: ci.stock }), color: "var(--color-warning)" } : null;
        return { icon: "package", label: t("inv.line.from.catalog"), warn };
      }
      return null;
    };

    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
        {/* client + project */}
        <div>
          <div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
            <Kicker style={{ flex: 1 }}>{t("inv.billto")}</Kicker>
            {headerRight}
          </div>
          {client ? (
            <button onClick={onPickClient} className="q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, padding: "10px 12px", border: "1.4px solid var(--color-border-strong)", borderRadius: 12, background: "var(--color-surface-0)", cursor: "pointer" }}>
              <Avatar name={client.name} size={38} square />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14, fontWeight: 600 }}>{client.name}</div>
                <div style={{ fontSize: 12, color: "var(--color-text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{client.email || t("cl.never")}</div>
              </div>
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--color-primary)" }}>{t("man.client.change")}</span>
            </button>
          ) : draft.newClientName ? (
            <button onClick={onPickClient} className="q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", background: "var(--color-accent-muted)", borderRadius: 12, border: "1.4px solid var(--color-accent)", cursor: "pointer" }}>
              <Avatar name={draft.newClientName} size={38} square />
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 14, fontWeight: 600 }}>{draft.newClientName}</div>
                <div style={{ fontSize: 12, color: "var(--color-accent)", fontWeight: 600 }}>{t("ai.newclient.chip")}</div>
              </div>
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--color-primary)" }}>{t("man.client.change")}</span>
            </button>
          ) : (
            <button onClick={onPickClient} className="q-tap" style={{ width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "1.4px dashed var(--color-border-strong)", borderRadius: 12, background: "var(--color-surface-0)", cursor: "pointer", color: "var(--color-text-2)" }}>
              <div style={{ width: 34, height: 34, borderRadius: 8, background: "var(--color-surface-1)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="clients" size={17} /></div>
              <span style={{ flex: 1, textAlign: "left", fontSize: 14, fontWeight: 600 }}>{t("man.client.pick")}</span>
              <Icon name="chevronRight" size={18} color="var(--color-text-3)" />
            </button>
          )}
          {client && (
            <button onClick={onPickProject} className="q-tap" style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, marginTop: 8, padding: "9px 12px", border: "1px solid var(--color-border)", borderRadius: 8, background: "transparent", cursor: "pointer" }}>
              <Icon name="briefcase" size={15} color="var(--color-text-3)" />
              <span style={{ flex: 1, textAlign: "left", fontSize: 13, color: project ? "var(--color-text-1)" : "var(--color-text-3)", fontWeight: project ? 600 : 500 }}>{project ? project.name : t("inv.noproject")}</span>
              <Icon name="chevronDown" size={15} color="var(--color-text-3)" />
            </button>
          )}
          {teamPending && (
            <button onClick={applyTeam} className="q-tap" style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, marginTop: 8, padding: "9px 12px", border: "1.4px dashed var(--color-border-strong)", borderRadius: 8, background: "transparent", cursor: "pointer", color: "var(--color-text-2)" }}>
              <Icon name="users" size={15} color="var(--color-text-3)" />
              <span style={{ flex: 1, textAlign: "left", fontSize: 13, fontWeight: 600 }}>{t("man.project.applyteam")}</span>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", fontVariantNumeric: "tabular-nums" }}>{(project.team || []).length}</span>
            </button>
          )}
        </div>

        {/* Nita noticed: unbilled billable expenses for this client */}
        {suggestExpenses && (
          <Card ai pad={13} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <AiKicker>{tx("man.ai.kicker", t("home.ai.kicker"))}</AiKicker>
            <div style={{ fontSize: 13, color: "var(--color-text-1)", lineHeight: 1.45 }}>
              {tx("man.ai.expenses", "I found billable expenses for " + client.name + ". You can add them to this invoice.", { client: client.name })}
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <Button variant="ai" size="sm" onClick={onAddExpense}>{t("man.addexpense")}</Button>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, color: "var(--color-accent)" }}>{t("home.expenses.tobill", { count: billableExp.length })}</span>
            </div>
          </Card>
        )}

        {/* dates: terms get a full-width row so chip labels never wrap mid-token (FR "15 jours") */}
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("inv.issued")}</Kicker>
            <div style={{ display: "flex", alignItems: "center", fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--color-text-2)" }}>{window.fmtDate(draft.issued)}</div>
          </div>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("inv.terms")}</Kicker>
            <div style={{ display: "flex", gap: 6 }}>
              {[0, 15, 30, 45].map(n => (
                <button key={n} onClick={() => up({ terms: n, due: addDays(draft.issued, n) })} style={{
                  flex: 1, minWidth: 0, height: 38, borderRadius: 8, fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, cursor: "pointer",
                  whiteSpace: "nowrap", lineHeight: 1, display: "flex", alignItems: "center", justifyContent: "center",
                  border: `1.4px solid ${draft.terms === n ? "var(--color-primary)" : "var(--color-border-strong)"}`,
                  background: draft.terms === n ? "var(--color-primary-muted)" : "var(--color-surface-0)",
                  color: draft.terms === n ? "var(--color-primary)" : "var(--color-text-2)",
                }}>{n === 0 ? tx("g.receipt", "rcpt") : t("g.net" + n)}</button>
              ))}
            </div>
          </div>
        </div>

        {/* repeats (recurrence rule) — invoices only; quotes/credits never recur */}
        {draft.kind === "invoice" && (
          <div>
            <Field label={t("rec.repeats")}>
              <Select value={draft.recurrence ? draft.recurrence.every : "none"}
                onChange={e => {
                  const v = e.target.value;
                  up({ recurrence: v === "none" ? null : { every: v, nextRun: LIB.nextCadence(draft.issued, v) } });
                }}
                options={[
                  { value: "none", label: t("rec.none") },
                  { value: "week", label: t("rec.every.week") },
                  { value: "month", label: t("rec.every.month") },
                  { value: "quarter", label: t("rec.every.quarter") },
                  { value: "year", label: t("rec.every.year") },
                ]} />
            </Field>
            {draft.recurrence && (
              <div style={{ marginTop: 6, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)" }}>
                {t("rec.next", { date: window.fmtDate(draft.recurrence.nextRun) })}
              </div>
            )}
          </div>
        )}

        {/* line items */}
        <div>
          <Kicker style={{ marginBottom: 2 }}>{t("inv.lineitems")}</Kicker>
          {draft.items.length === 0 && (
            <div style={{ padding: "14px 0", fontSize: 13, color: "var(--color-text-3)" }}>{t("man.empty.items")}</div>
          )}
          {draft.items.map((it, i) => (
            <LineItem key={it.id} it={it} currency={draft.currency} meta={lineMeta(it)} onViewReceipt={onViewReceipt} taxes={taxes} onAddTax={onAddTax} onTaxPicker={onTaxPicker} vatLocked={vatLocked}
              onChange={ni => setItem(it.id, ni)} onRemove={() => removeItem(it.id)}
              onMoveUp={() => move(i, -1)} onMoveDown={() => move(i, 1)} canUp={i > 0} canDown={i < draft.items.length - 1} />
          ))}
          <div style={{ display: "flex", flexWrap: "wrap", gap: "10px 18px", marginTop: 12 }}>
            <button onClick={addLine} style={{ display: "flex", alignItems: "center", gap: 6, background: "none", border: "none", color: "var(--color-primary)", fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0 }}>
              <Icon name="plusCircle" size={17} />{t("man.addline")}
            </button>
            {onAddCatalog && bd.showCatalog && (
              <button onClick={onAddCatalog} style={{ display: "flex", alignItems: "center", gap: 6, background: "none", border: "none", color: "var(--color-primary)", fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0 }}>
                <Icon name="package" size={16} />{t("man.addcatalog")}
              </button>
            )}
            {onAddExpense && client && (
              <button onClick={onAddExpense} style={{ display: "flex", alignItems: "center", gap: 6, background: "none", border: "none", color: "var(--color-primary)", fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0 }}>
                <Icon name="receipt" size={16} />{t("man.addexpense")}
              </button>
            )}
          </div>
        </div>

        {/* currency + VAT + invoice discount */}
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div style={{ display: "flex", gap: 12 }}>
            <div style={{ width: 96 }}>
              <Kicker style={{ marginBottom: 6 }}>{t("inv.currency")}</Kicker>
              <Select value={draft.currency} onChange={e => up({ currency: e.target.value })} options={curOpts} style={{ height: 38, fontSize: 13 }} />
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <Kicker style={{ marginBottom: 6 }}>{t("inv.vat")} %</Kicker>
              <TaxField value={bulkTaxValue} taxes={taxes} onAddTax={onAddTax} onToggle={onTaxPicker} disabled={vatLocked}
                onChange={({ rate, name }) => up({ taxRate: rate, taxName: name, items: draft.items.map(x => ({ ...x, taxRate: rate, taxName: name })) })} />
            </div>
          </div>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("man.vatmode.label")}</Kicker>
            <Segmented value={draft.vatMode || "standard"} onChange={m => up({ vatMode: m })}
              options={[{ value: "standard", label: t("man.vatmode.standard") }, { value: "reverse", label: t("man.vatmode.reverse") }, { value: "exempt", label: t("man.vatmode.exempt") }]} />
            {(draft.vatMode === "reverse" || draft.vatMode === "exempt") && (
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 8, padding: "9px 12px", background: "var(--color-surface-1)", borderRadius: 8 }}>
                <Icon name="percent" size={15} color="var(--color-text-3)" />
                <span style={{ flex: 1, fontSize: 13, color: "var(--color-text-2)", lineHeight: 1.4 }}>{t(LIB.vatModeMention(draft.vatMode))}</span>
              </div>
            )}
            {suggestReverse && (
              <Card ai pad={13} style={{ marginTop: 10 }}>
                <Q.AiKicker>{t("man.ai.kicker")}</Q.AiKicker>
                <div style={{ fontSize: 13, color: "var(--color-text-1)", lineHeight: 1.45, marginTop: 7 }}>
                  {t("man.ai.reverse", { client: client.name, country: buyerVat })}
                </div>
                <div style={{ display: "flex", gap: 8, marginTop: 11 }}>
                  <Button variant="ai" size="sm" onClick={() => { up({ vatMode: "reverse" }); setRcDismissed(true); }}>{t("man.ai.reverse.apply")}</Button>
                  <Button variant="ghost" size="sm" onClick={() => setRcDismissed(true)}>{t("man.ai.reverse.dismiss")}</Button>
                </div>
              </Card>
            )}
          </div>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("man.invlevel")} %</Kicker>
            <NumCell value={draft.discountPct} onChange={v => up({ discountPct: v })} width="100%" suffix="%" />
          </div>
        </div>

        {/* totals */}
        <div style={{ paddingTop: 14, borderTop: "1px solid var(--color-border)", display: "flex", flexDirection: "column", gap: 8 }}>
          <TotRow label={t("inv.subtotal")} value={tot.subtotal} currency={draft.currency} />
          {tot.discount > 0 && <TotRow label={`${t("inv.discount")} (${LIB.fmtPct(draft.discountPct)})`} value={-tot.discount} currency={draft.currency} />}
          {tot.vatBreakdown.filter(v => v.rate > 0).map(v => (
            <TotRow key={v.name + "_" + v.rate} label={v.name ? `${v.name} (${LIB.fmtPct(v.rate)})` : `${t("inv.vat")} ${LIB.fmtPct(v.rate)}`} value={v.tax} currency={draft.currency} />
          ))}
          <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginTop: 4 }}>
            <span style={{ fontSize: 14, fontWeight: 700, color: "var(--color-text-1)" }}>{t("inv.total")}</span>
            <MoneyDisplay amount={tot.total} currency={draft.currency} size={22} />
          </div>
        </div>

        {/* notes + legal */}
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("inv.notes")}</Kicker>
            <Textarea value={draft.notes} onChange={e => up({ notes: e.target.value })} rows={2} placeholder={defaultNotes(db.COMPANY, draft.terms)} />
          </div>
          <div>
            <Kicker style={{ marginBottom: 6 }}>{t("inv.legal")}</Kicker>
            <Textarea value={draft.legal} onChange={e => up({ legal: e.target.value })} rows={2} placeholder={(db.COMPANY.legalMention || "").slice(0, 80)} />
          </div>
        </div>
      </div>
    );
  }
  function TotRow({ label, value, currency }) {
    return (
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <span style={{ fontSize: 13, color: "var(--color-text-2)" }}>{label}</span>
        <Money amount={value} currency={currency} size={13} color="var(--color-text-1)" weight={500} />
      </div>
    );
  }

  /* ── Client picker sheet (search · pick · full new-client form) ── */
  function ClientPicker({ api, open, onClose, onPick, currency, mode }) {
    const { db, setDb, today } = api;
    const [q, setQ] = useState("");
    const [view, setView] = useState("list"); // list | new
    const [form, setForm] = useState(null);
    const ql = q.trim().toLowerCase();
    const list = db.CLIENTS.filter(c => !ql || c.name.toLowerCase().includes(ql) || (c.sector || "").toLowerCase().includes(ql));
    const exact = db.CLIENTS.some(c => c.name.toLowerCase() === ql);

    // reset whenever the sheet (re)opens — manage mode opens straight to the form
    useEffect(() => {
      if (open) {
        setQ("");
        if (mode === "manage") { setForm({ ...blankClient(currency || db.COMPANY.currency) }); setView("new"); }
        else setView("list");
      }
    }, [open]);

    const startNew = (prefill) => { setForm({ ...blankClient(currency || db.COMPANY.currency), name: prefill || "" }); setView("new"); };
    const createClient = (client) => { setDb(d => ({ ...d, CLIENTS: [...d.CLIENTS, client] })); onPick(client.id); };
    const quickAdd = () => createClient({ ...blankClient(currency || db.COMPANY.currency), name: q.trim().replace(/\b\w/g, c => c.toUpperCase()) });
    const saveForm = () => {
      if (!form.name.trim()) return;
      createClient({ ...form, name: form.name.trim() });
      Q.toast(t("toast.added", { name: form.name.trim() }), "check");
    };
    const setF = patch => setForm(f => ({ ...f, ...patch }));

    if (view === "new" && form) {
      return (
        <Sheet open={open} onClose={onClose} height="92%" title={t("man.client.form")}
          footer={<Button variant="primary" size="lg" full icon="check" disabled={!form.name.trim()} onClick={saveForm}>{t("man.client.save")}</Button>}>
          <div style={{ padding: "2px 16px 16px" }}>
            <button onClick={() => setView("list")} style={{ display: "flex", alignItems: "center", gap: 4, background: "none", border: "none", color: "var(--color-text-2)", cursor: "pointer", padding: 0, fontSize: 13, fontWeight: 600, marginBottom: 16 }}>
              <Icon name="chevronLeft" size={16} />{t("g.back")}
            </button>
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <Field label={t("ob.profile.name")}><Input value={form.name} onChange={e => setF({ name: e.target.value })} placeholder={t("man.client.name.ph")} /></Field>
              <Field label={t("man.client.contact")}><Input value={form.contact} onChange={e => setF({ contact: e.target.value })} placeholder={t("proj.member.ph")} /></Field>
              <Field label={t("cl.email")} hint={tx("man.client.email.hint", "Used when you send the invoice.")}><Input value={form.email} onChange={e => setF({ email: e.target.value })} type="email" placeholder="billing@client.com" /></Field>
              <Field label={t("cl.phone")}><Input value={form.phone} onChange={e => setF({ phone: e.target.value })} mono placeholder="+33 …" /></Field>
              <Field label={t("cl.address")}><Textarea value={form.address} onChange={e => setF({ address: e.target.value })} rows={2} placeholder={tx("cl.address.ph", "Street\nPostcode, City, Country")} /></Field>
              <div style={{ display: "flex", gap: 12 }}>
                <Field label={t("cl.taxid")} style={{ flex: 1 }}><Input value={form.vat} onChange={e => setF({ vat: e.target.value })} mono /></Field>
                <Field label={t("cl.reg")} style={{ flex: 1 }}><Input value={form.reg} onChange={e => setF({ reg: e.target.value })} mono /></Field>
              </div>
            </div>
          </div>
        </Sheet>
      );
    }

    return (
      <Sheet open={open} onClose={onClose} height="86%" title={t("man.client.pick")}>
        <div style={{ padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 12 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, height: 44, padding: "0 14px", background: "var(--color-surface-0)", border: "1.4px solid var(--color-border-strong)", borderRadius: 999 }}>
            <Icon name="search" size={17} color="var(--color-text-3)" />
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder={t("man.client.search")}
              onFocus={e => e.target.parentNode.style.borderColor = "var(--color-primary)"}
              onBlur={e => e.target.parentNode.style.borderColor = "var(--color-border-strong)"}
              style={{ flex: 1, border: "none", background: "transparent", outline: "none", fontSize: 15, color: "var(--color-text-1)", fontFamily: "var(--font-sans)" }} />
          </div>

          {/* New client (full form) */}
          <button onClick={() => startNew(ql && !exact ? q.trim().replace(/\b\w/g, c => c.toUpperCase()) : "")} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 13px", border: "1.4px solid var(--color-primary)", borderRadius: 12, background: "var(--color-primary-muted)", cursor: "pointer" }}>
            <div style={{ width: 38, height: 38, borderRadius: 8, background: "var(--color-primary)", color: "var(--color-primary-fg)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name="plus" size={18} /></div>
            <div style={{ flex: 1, textAlign: "left" }}>
              <div style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-1)" }}>{t("man.client.new")}</div>
              <div style={{ fontSize: 12, color: "var(--color-text-2)" }}>{tx("man.client.new.sub", "Name, email, address, tax IDs")}</div>
            </div>
          </button>
          {/* quick add typed name */}
          {ql && !exact && (
            <button onClick={quickAdd} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 13px", border: "1px solid var(--color-border)", borderRadius: 12, background: "var(--color-surface-0)", cursor: "pointer" }}>
              <Icon name="arrowRight" size={15} color="var(--color-text-3)" />
              <span style={{ flex: 1, textAlign: "left", fontSize: 13, fontWeight: 600, color: "var(--color-text-2)" }}>{t("man.client.quick", { name: q.trim() })}</span>
            </button>
          )}

          <div style={{ border: "1.4px solid var(--color-border-strong)", borderRadius: 12, overflow: "hidden" }}>
            {list.map((c, i) => {
              const bal = LIB.clientBalance(db, c.id, today);
              return (
                <button key={c.id} onClick={() => onPick(c.id)} className="q-row q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "none", borderBottom: i < list.length - 1 ? "1px solid var(--color-border)" : "none", background: "var(--color-surface-0)", cursor: "pointer" }}>
                  <Avatar name={c.name} size={38} square />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600 }}>{c.name}</div>
                    <div style={{ fontSize: 12, color: "var(--color-text-3)" }}>{c.email || c.sector || "-"}</div>
                  </div>
                  {bal > 0 && <Money amount={bal} currency={c.currency} size={13} cents={false} />}
                </button>
              );
            })}
            {list.length === 0 && <div style={{ padding: 20, textAlign: "center", fontSize: 13, color: "var(--color-text-3)" }}>{tx("man.client.nomatch", "No matches. Add them as a new client above.")}</div>}
          </div>
        </div>
      </Sheet>
    );
  }

  /* ── Project picker sheet (pick · add new) ───────────────────── */
  function ProjectPicker({ api, open, client, onClose, onPick, currency }) {
    const [view, setView] = useState("list");
    const [name, setName] = useState("");
    const [basis, setBasis] = useState("rate"); // rate | budget | none
    const [amount, setAmount] = useState("");
    useEffect(() => { if (open) { setView("list"); setName(""); setBasis("rate"); setAmount(""); } }, [open]);
    if (!client) return null;
    const cur = currency || client.currency || api.db.COMPANY.currency;

    const addProject = () => {
      if (!name.trim()) return;
      const id = "p" + Date.now().toString(36);
      const proj = { id, name: name.trim(), status: "active" };
      if (basis === "rate") proj.rate = Number(amount) || 0;
      else if (basis === "budget") proj.budget = Number(amount) || 0;
      api.setDb(d => ({ ...d, CLIENTS: d.CLIENTS.map(c => c.id === client.id ? { ...c, projects: [...(c.projects || []), proj] } : c) }));
      onPick(id);
      Q.toast(t("toast.added", { name: proj.name }), "check");
    };

    if (view === "new") {
      return (
        <Sheet open={open} onClose={onClose} height="auto" title={t("man.proj.new")}
          footer={<Button variant="primary" size="lg" full icon="check" disabled={!name.trim()} onClick={addProject}>{t("man.proj.add")}</Button>}>
          <div style={{ padding: "2px 16px 16px", display: "flex", flexDirection: "column", gap: 14 }}>
            <button onClick={() => setView("list")} style={{ display: "flex", alignItems: "center", gap: 4, background: "none", border: "none", color: "var(--color-text-2)", cursor: "pointer", padding: 0, fontSize: 13, fontWeight: 600 }}>
              <Icon name="chevronLeft" size={16} />{t("g.back")}
            </button>
            <Field label={t("man.proj.name")}><Input value={name} onChange={e => setName(e.target.value)} placeholder={t("man.proj.name.ph")} autoFocus /></Field>
            <div>
              <Kicker style={{ marginBottom: 8 }}>{t("man.proj.basis")}</Kicker>
              <Segmented value={basis} onChange={setBasis} options={[{ value: "rate", label: t("man.proj.rate") }, { value: "budget", label: t("man.proj.budget") }, { value: "none", label: t("man.proj.none") }]} />
            </div>
            {basis !== "none" && (
              <Field label={t("man.proj.amount")}>
                <div style={{ position: "relative" }}>
                  <span style={{ position: "absolute", left: 14, top: "50%", transform: "translateY(-50%)", fontFamily: "var(--font-mono)", fontSize: 15, color: "var(--color-text-3)" }}>{(window.CURRENCIES[cur] || {}).symbol}</span>
                  <Input value={amount} onChange={e => setAmount(e.target.value.replace(/[^\d.]/g, ""))} mono inputMode="decimal" style={{ paddingLeft: 34 }} placeholder={basis === "rate" ? t("man.proj.rate") : t("man.proj.budget")} />
                </div>
              </Field>
            )}
          </div>
        </Sheet>
      );
    }

    return (
      <Sheet open={open} onClose={onClose} height="auto" title={t("inv.project")}>
        <div style={{ padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 8 }}>
          <button onClick={() => setView("new")} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "1.4px solid var(--color-primary)", borderRadius: 12, background: "var(--color-primary-muted)", cursor: "pointer" }}>
            <div style={{ width: 34, height: 34, borderRadius: 8, background: "var(--color-primary)", color: "var(--color-primary-fg)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="plus" size={17} /></div>
            <span style={{ flex: 1, textAlign: "left", fontSize: 14, fontWeight: 600, color: "var(--color-text-1)" }}>{t("man.proj.new")}</span>
          </button>
          <button onClick={() => onPick(null)} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "1.4px solid var(--color-border-strong)", borderRadius: 12, background: "var(--color-surface-0)", cursor: "pointer" }}>
            <span style={{ flex: 1, textAlign: "left", fontSize: 14, fontWeight: 600 }}>{t("inv.noproject")}</span>
          </button>
          {(client.projects || []).map(p => (
            <button key={p.id} onClick={() => onPick(p.id)} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "1.4px solid var(--color-border-strong)", borderRadius: 12, background: "var(--color-surface-0)", cursor: "pointer" }}>
              <div style={{ width: 34, height: 34, borderRadius: 8, background: "var(--color-surface-1)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--color-text-3)" }}><Icon name="briefcase" size={16} /></div>
              <div style={{ flex: 1, textAlign: "left" }}>
                <div style={{ fontSize: 14, fontWeight: 600 }}>{p.name}</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-gold)" }}>{p.rate ? t("cl.proj.rate", { rate: window.fmtMoney(p.rate, cur, { cents: false }) }) : p.budget ? t("cl.proj.budget", { amount: window.fmtMoney(p.budget, cur, { cents: false }) }) : "-"}</div>
              </div>
            </button>
          ))}
        </div>
      </Sheet>
    );
  }

  /* ── Catalog picker sheet (search · stock cues · tap to add) ──── */
  function CatalogPicker({ api, open, onClose, onAdd }) {
    const { db } = api;
    const [q, setQ] = useState("");
    useEffect(() => { if (open) setQ(""); }, [open]);
    const ql = q.trim().toLowerCase();
    const all = db.CATALOG || [];
    const list = all.filter(it => !ql || it.name.toLowerCase().includes(ql) || (it.sku || "").toLowerCase().includes(ql));

    const stockCue = (it) => {
      const st = LIB.stockState(it);
      if (st === "none") return null;
      const color = st === "out" ? "var(--color-error)" : st === "low" ? "var(--color-warning)" : "var(--color-text-1)";
      const txt = st === "out" ? t("cat.stock.out") : st === "low" ? t("cat.stock.low") : t("cat.stock.in", { n: it.stock });
      return (
        <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, color, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>
          {st !== "ok" && <span style={{ width: 5, height: 5, borderRadius: "50%", background: "currentColor", flexShrink: 0 }} />}
          {txt}
        </span>
      );
    };

    return (
      <Sheet open={open} onClose={onClose} height="86%" title={t("man.catalog.title")}>
        <div style={{ padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 12 }}>
          {all.length === 0 ? (
            <EmptyState icon="package" title={t("cat.empty.title")} body={t("man.catalog.empty")} />
          ) : (
            <React.Fragment>
              <div style={{ display: "flex", alignItems: "center", gap: 8, height: 44, padding: "0 14px", background: "var(--color-surface-0)", border: "1.4px solid var(--color-border-strong)", borderRadius: 999 }}>
                <Icon name="search" size={17} color="var(--color-text-3)" />
                <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder={t("man.catalog.search")}
                  onFocus={e => e.target.parentNode.style.borderColor = "var(--color-primary)"}
                  onBlur={e => e.target.parentNode.style.borderColor = "var(--color-border-strong)"}
                  style={{ flex: 1, border: "none", background: "transparent", outline: "none", fontSize: 15, color: "var(--color-text-1)", fontFamily: "var(--font-sans)" }} />
              </div>
              <div style={{ border: "1.4px solid var(--color-border-strong)", borderRadius: 12, overflow: "hidden" }}>
                {list.map((it, i) => (
                  <button key={it.id} onClick={() => onAdd(it)} className="q-row q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", border: "none", borderBottom: i < list.length - 1 ? "1px solid var(--color-border)" : "none", background: "var(--color-surface-0)", cursor: "pointer" }}>
                    <div style={{ width: 38, height: 38, borderRadius: 8, background: "var(--color-surface-1)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--color-text-3)", flexShrink: 0 }}>
                      <Icon name={it.kind === "service" ? "briefcase" : "package"} size={17} />
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.name}</div>
                      <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 2, minWidth: 0 }}>
                        {it.sku && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", flexShrink: 0 }}>{it.sku}</span>}
                        {stockCue(it)}
                      </div>
                    </div>
                    <div style={{ textAlign: "right", flexShrink: 0 }}>
                      <Money amount={it.price} currency={it.currency || db.COMPANY.currency} size={13} cents={!Number.isInteger(Number(it.price))} />
                      <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--color-text-3)", marginTop: 2 }}>/ {LIB.unitLabel(it.unit, 1)}</div>
                    </div>
                  </button>
                ))}
                {list.length === 0 && <div style={{ padding: 20, textAlign: "center", fontSize: 13, color: "var(--color-text-3)" }}>{t("g.nomatch")}</div>}
              </div>
            </React.Fragment>
          )}
        </div>
      </Sheet>
    );
  }

  /* ── Billable-expense picker sheet (per client, receipt thumbs) ── */
  function ExpensePicker({ api, open, onClose, onAdd, clientId }) {
    const { db } = api;
    const list = (db.EXPENSES || []).filter(e => e.billable && e.status === "unbilled" && e.clientId === clientId);
    return (
      <Sheet open={open} onClose={onClose} height="auto" title={t("man.expense.pick")}>
        <div style={{ padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 12 }}>
          {list.length === 0 ? (
            <div style={{ padding: "26px 20px", textAlign: "center", fontSize: 13, color: "var(--color-text-3)", lineHeight: 1.5 }}>{t("man.expense.none")}</div>
          ) : (
            <div style={{ border: "1.4px solid var(--color-border-strong)", borderRadius: 12, overflow: "hidden" }}>
              {list.map((e, i) => (
                <div key={e.id} role="button" tabIndex={0} onClick={() => onAdd(e)}
                  onKeyDown={ev => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); onAdd(e); } }}
                  className="q-row q-tap" style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, padding: "12px 14px", borderBottom: i < list.length - 1 ? "1px solid var(--color-border)" : "none", background: "var(--color-surface-0)", cursor: "pointer", boxSizing: "border-box" }}>
                  {e.receipt && e.receipt.src ? (
                    <ReceiptThumb receipt={e.receipt} size={30} style={{ pointerEvents: "none" }} />
                  ) : (
                    <div style={{ width: 30, height: 39, borderRadius: 8, background: "var(--color-surface-1)", border: "1.4px dashed var(--color-border-strong)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--color-text-3)", flexShrink: 0 }}>
                      <Icon name="receipt" size={14} />
                    </div>
                  )}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.merchant}</div>
                    <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 2 }}>{window.fmtDate(e.date, "short")} · {t("exp.cat." + e.category)}</div>
                  </div>
                  <Money amount={e.amount} currency={e.currency} size={13} />
                </div>
              ))}
            </div>
          )}
        </div>
      </Sheet>
    );
  }

  /* ── Manual editor (full-screen) ─────────────────────────────── */
  function ManualEditor({ api, seed, onClose }) {
    const { db, setDb, today } = api;
    const co = db.COMPANY;
    /* Company default tax = the config tax flagged default (carries its name),
       falling back to a rate match then co.taxRate so the seed line has identity. */
    const cfgTaxes = (db.config && db.config.taxes) || co.taxes || [];
    const defaultTax = cfgTaxes.find(tx => tx.default) || cfgTaxes.find(tx => Number(tx.rate) === Number(co.taxRate)) || null;
    const defTaxRate = defaultTax ? defaultTax.rate : co.taxRate;
    const defTaxName = defaultTax ? defaultTax.name : "";
    const [shown, setShown] = useState(false);
    const [draft, setDraft] = useState(() => {
      const base = {
        kind: "invoice", clientId: null, newClientName: null, projectId: null,
        issued: today, terms: co.terms, due: addDays(today, co.terms), currency: co.currency, status: "draft", vatMode: "standard",
        items: [{ id: uid(), description: "", qty: 1, rate: 0, unit: LIB.businessDefaults(co).defaultUnit, discountPct: 0, taxRate: defTaxRate, taxName: defTaxName }],
        discountPct: 0, taxRate: defTaxRate, taxName: defTaxName, notes: "", legal: "", paid: 0,
        recurrence: null, quoteId: null, depositOf: null, depositPct: null, creditFor: null,
      };
      if (!seed) return base;
      const merged = { ...base, ...seed };
      if (!seed.items || !seed.items.length) merged.items = base.items;
      if (seed.terms == null) merged.terms = base.terms;
      if (!seed.due) merged.due = addDays(merged.issued || today, merged.terms);
      if (seed.kind === "quote") merged.status = "quote";
      return merged;
    });
    const [pickClient, setPickClient] = useState(false);
    const [pickProject, setPickProject] = useState(false);
    const [pickCatalog, setPickCatalog] = useState(false);
    const [pickExpense, setPickExpense] = useState(false);
    const [taxPickerOpen, setTaxPickerOpen] = useState(false);
    const [viewReceipt, setViewReceipt] = useState(null);
    const [err, setErr] = useState(null);
    const timer = useRef(null);
    const bodyRef = useRef(null);
    React.useEffect(() => { const r = setTimeout(() => setShown(true), 20); return () => { clearTimeout(r); clearTimeout(timer.current); }; }, []);

    const close = () => { setShown(false); timer.current = setTimeout(onClose, 280); };

    /* Escape closes the overlay (parity with the Sheet trapKeys pattern).
       Nested sheets handle Escape themselves and stop propagation; this
       listener is also disabled while any nested sheet/viewer is open. */
    const sheetOpen = pickClient || pickProject || pickCatalog || pickExpense || taxPickerOpen || !!viewReceipt;
    React.useEffect(() => {
      if (sheetOpen) return;
      const onKey = (e) => { if (e.key === "Escape") { e.stopPropagation(); close(); } };
      document.addEventListener("keydown", onKey);
      return () => document.removeEventListener("keydown", onKey);
    }, [sheetOpen]);
    const client = draft.clientId ? LIB.getClient(db, draft.clientId) : null;
    const isQuote = draft.kind === "quote";

    const onPickClient = (clientId) => {
      setDraft(dr => ({ ...dr, clientId, newClientName: null, projectId: null }));
      setPickClient(false);
    };
    /* prefill from catalog / billable expenses (drops a blank starter row) */
    const realItems = items => items.filter(x => (x.description && String(x.description).trim()) || Number(x.rate) > 0);
    const addCatalogLine = (item) => {
      setDraft(dr => ({ ...dr, items: [...realItems(dr.items), LIB.catalogToLine(item, 1)] }));
      setPickCatalog(false);
      Q.toast(t("toast.addedToInvoice"), "check");
    };
    const addExpenseLine = (e) => {
      setDraft(dr => ({ ...dr, items: [...realItems(dr.items), LIB.expenseToLine(e)] }));
      setPickExpense(false);
      Q.toast(t("toast.addedToInvoice"), "check");
    };

    /* validation feedback renders at the top of the scrollable body; bring
       it into view so a scrolled-down user always sees why save blocked */
    const showErr = (msg) => {
      setErr(msg);
      if (bodyRef.current) bodyRef.current.scrollTo({ top: 0, behavior: "smooth" });
    };
    const save = () => {
      /* Missing client: open the picker (which IS the fix for the missing
         data) rather than flashing an inline banner the sheet would cover. */
      if (!draft.clientId && !draft.newClientName) { setErr(null); setPickClient(true); return; }
      const priced = draft.items.filter(it => (Number(it.qty) || 0) > 0 && (Number(it.rate) || 0) > 0);
      if (priced.length === 0) { showErr(t("man.valid.items")); return; }
      setErr(null);
      let working = db;
      let clientId = draft.clientId;
      if (!clientId && draft.newClientName) {
        clientId = "c" + Date.now().toString(36);
        const nc = { id: clientId, name: draft.newClientName, contact: "", email: "", phone: "", address: "", vat: "", reg: "", sector: "", currency: draft.currency, projects: [] };
        working = { ...working, CLIENTS: [...working.CLIENTS, nc] };
      }
      /* cleanItem preserves provenance (source / catalogId / expenseId / memberId / receipt) */
      const items = draft.items.filter(it => it.description.trim() || (Number(it.rate) || 0) > 0).map(it => LIB.cleanItem(it, draft));
      /* persist any off-list rate (typed 10%) into config so it stays selectable */
      [{ rate: draft.taxRate, name: draft.taxName }, ...draft.items.map(it => ({ rate: it.taxRate, name: it.taxName }))]
        .forEach(tx => { if (tx && Number(tx.rate) > 0) working = LIB.ensureConfigTax(working, { rate: Number(tx.rate), name: tx.name || "VAT" }); });
      const notes = draft.notes || defaultNotes(working.COMPANY, draft.terms);
      const vatMode = draft.vatMode || "standard";
      const parties = LIB.snapshotParties(working.COMPANY, clientId ? LIB.getClient(working, clientId) : null, draft.newClientName);

      if (draft.editId) {
        // update in place, keep the audit trail and log the edit
        const prev = working.INVOICES.find(x => x.id === draft.editId) || {};
        let updated = { ...prev, clientId, projectId: draft.projectId, issued: draft.issued, due: draft.due, currency: draft.currency, items, discountPct: Number(draft.discountPct) || 0, taxRate: draft.taxRate || 0, notes, legal: draft.legal, seller: parties.seller, buyer: parties.buyer, vatMode, recurrence: draft.recurrence || null };
        updated = LIB.pushAudit(updated, LIB.auditEvent("edited"));
        const b = billExpenses(working, updated);
        setDb({ ...b.working, INVOICES: working.INVOICES.map(x => x.id === draft.editId ? b.inv : x) });
        const id = draft.editId;
        onClose();
        api.goTab("invoices", "invoice-preview", { id });
        Q.toast(t("toast.invoiceUpdated"), "check");
        return;
      }
      const number = LIB.nextNumber(working.COMPANY, draft.kind);
      let inv = {
        id: number, kind: draft.kind, clientId, projectId: draft.projectId, issued: draft.issued, due: draft.due,
        currency: draft.currency, status: draft.kind === "quote" ? "quote" : "draft",
        items, discountPct: Number(draft.discountPct) || 0, taxRate: draft.taxRate || 0, paid: 0, notes, legal: draft.legal,
        seller: parties.seller, buyer: parties.buyer, vatMode,
        recurrence: draft.recurrence || null,
        quoteId: draft.quoteId || null, depositOf: draft.depositOf || null,
        depositPct: draft.depositPct != null ? draft.depositPct : null, creditFor: draft.creditFor || null,
        audit: [LIB.auditEvent("created")],
      };
      const b = billExpenses(working, inv);
      working = b.working; inv = b.inv;
      const act = LIB.activityEvent("created", { id: number }, number);
      setDb({
        ...working,
        COMPANY: { ...working.COMPANY, ...(draft.kind === "quote"
          ? { nextQuoteSeq: (working.COMPANY.nextQuoteSeq != null ? working.COMPANY.nextQuoteSeq : working.COMPANY.nextSeq) + 1 }
          : draft.kind === "credit"
            ? { nextCreditSeq: (working.COMPANY.nextCreditSeq != null ? working.COMPANY.nextCreditSeq : working.COMPANY.nextSeq) + 1 }
            : { nextSeq: working.COMPANY.nextSeq + 1 }) },
        INVOICES: [inv, ...working.INVOICES],
        ACTIVITY: [act, ...working.ACTIVITY],
      });
      onClose();
      api.goTab("invoices", "invoice-preview", { id: number, fresh: true });
    };

    return (
      <div style={{ position: "absolute", inset: 0, zIndex: 130, background: "var(--color-bg)", display: "flex", flexDirection: "column", transform: shown ? "translateY(0)" : "translateY(100%)", transition: "transform .34s cubic-bezier(.32,.72,0,1)", overflow: "hidden" }}>
        {/* header */}
        <div style={{ paddingTop: Q.SAFE_TOP, paddingLeft: 16, paddingRight: 12, paddingBottom: 10, display: "flex", alignItems: "center", gap: 8, borderBottom: "1px solid var(--color-border)" }}>
          <Icon name="keyboard" size={18} color="var(--color-text-2)" />
          <div style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 19, flex: 1, color: "var(--color-text-1)" }}>{draft.kind === "credit" ? t("man.title.credit") : draft.editId ? t("g.edit") : isQuote ? t("man.title.quote") : t("man.title")}</div>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)" }}>{draft.editId || LIB.nextNumber(co, draft.kind)}</span>
          <IconButton name="x" onClick={close} label={t("g.close")} style={{ background: "var(--color-surface-1)" }} />
        </div>

        {/* body */}
        <div ref={bodyRef} style={{ flex: 1, overflowY: "auto", padding: "18px 16px 24px" }}>
          {err && (
            <div className="q-fade" style={{ display: "flex", alignItems: "center", gap: 8, padding: "11px 14px", marginBottom: 16, background: "var(--color-accent-muted)", border: "1.4px solid var(--color-accent)", borderRadius: 12 }}>
              <Icon name="alert" size={16} color="var(--color-accent)" />
              <span style={{ fontSize: 13, color: "var(--color-text-1)", fontWeight: 500 }}>{err}</span>
            </div>
          )}
          <EditorBody draft={draft} setDraft={setDraft} db={db}
            onPickClient={() => setPickClient(true)} onPickProject={() => setPickProject(true)}
            onAddCatalog={() => setPickCatalog(true)} onAddExpense={() => setPickExpense(true)}
            onViewReceipt={r => setViewReceipt(r)} onTaxPicker={setTaxPickerOpen}
            onAddTax={tx => api.setDb(d => LIB.ensureConfigTax(d, tx))}
            headerRight={!draft.editId && draft.kind !== "credit" ? (
              <Q.Segmented value={draft.kind} onChange={k => setDraft(dr => ({ ...dr, kind: k, status: k === "quote" ? "quote" : "draft" }))}
                options={[{ value: "invoice", label: t("man.type.invoice") }, { value: "quote", label: t("man.type.quote") }]}
                style={{ width: 168 }} />
            ) : null} />
        </div>

        {/* footer */}
        <div style={{ borderTop: "1px solid var(--color-border)", background: "var(--color-surface-0)", padding: "12px 16px 26px" }}>
          <Button variant="primary" size="lg" full icon="arrowRight" onClick={save}>{draft.editId ? t("man.done") : t("man.save")}</Button>
        </div>

        <ClientPicker api={api} open={pickClient} onClose={() => setPickClient(false)} onPick={onPickClient} currency={draft.currency} />
        <ProjectPicker api={api} open={pickProject} client={client} currency={draft.currency} onClose={() => setPickProject(false)} onPick={(pid) => { setDraft(dr => ({ ...dr, projectId: pid })); setPickProject(false); }} />
        <CatalogPicker api={api} open={pickCatalog} onClose={() => setPickCatalog(false)} onAdd={addCatalogLine} />
        <ExpensePicker api={api} open={pickExpense} onClose={() => setPickExpense(false)} onAdd={addExpenseLine} clientId={draft.clientId} />
        <ReceiptViewer open={!!viewReceipt} receipt={viewReceipt} onClose={() => setViewReceipt(null)} />
      </div>
    );
  }

  window.QEditor = { EditorBody, TaxField, TaxPickerSheet, ClientPicker, ProjectPicker, CatalogPicker, ExpensePicker, draftFromInvoice, billExpenses };
  window.QScreens = Object.assign(window.QScreens || {}, { ManualEditor });
})();
