// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /payments/reports/ · AdminPaymentsController::show('reports') → reportsData() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). Despite the filename this is the
   PayBO REPORTS page (docs/PROTOTYPE_INVENTORY.md lists it in the trap list).
   Real surface: reportsFilters() + reportsData(), exporting through
   GET /payments/reports/export, which streams a per-brand table of
   dep_amount / dep_count / wd_amount / wd_count / profit. Nothing here maps
   to the host ReportsController — those are the /reports/* screens. */
/* Reports & analytics — owner-facing snapshot + 4 focused areas.

   5 tabs, max 4–5 reports per tab. Every report:
     - has a one-line description so the owner knows what they're seeing
     - filters dynamically by the Daily / Weekly / Monthly / Custom range
       + the top-right brand selector
     - exports its data to CSV via a small download button in the corner
*/

/* ---------- shared helpers ---------- */

const ReportCard = ({ title, desc, exportRows, exportCols, exportFilename, children, actions }) => {
  const handleExport = () => {
    if (!window.PAYBO || !exportRows || !exportCols) return;
    const rows = typeof exportRows === "function" ? exportRows() : exportRows;
    const stamp = new Date().toISOString().slice(0, 10);
    window.PAYBO.downloadCSV(`${exportFilename || "report"}-${stamp}.csv`, rows, exportCols);
  };
  return (
    <div className="panel">
      <div className="section__head">
        <div>
          <div className="section__title">{title}</div>
          {desc && <div className="section__desc">{desc}</div>}
        </div>
        <div className="section__actions" style={{display:"flex", gap:6, alignItems:"center"}}>
          {actions}
          {exportRows && exportCols && (
            <button className="btn btn--secondary btn--sm" onClick={handleExport} title="Download as CSV">
              <Icon name="download" size={11}/> CSV
            </button>
          )}
        </div>
      </div>
      <div style={{padding:"14px 16px 16px"}}>{children}</div>
    </div>
  );
};

const KpiCard = ({ label, value, sub, tone }) => {
  const palette = {
    primary: { fg:"var(--paybo-heading, #1e3a8a)", strip:"var(--primary, #1e40af)" },
    ok:      { fg:"var(--ok-700, #065f46)",        strip:"var(--ok-500, #10b981)" },
    warn:    { fg:"var(--warn-700, #92400e)",      strip:"var(--warn-500, #f59e0b)" },
    err:     { fg:"var(--err-700, #991b1b)",       strip:"var(--err-500, #ef4444)" },
    neutral: { fg:"var(--text-primary)",           strip:"var(--text-tertiary)" },
  };
  const c = palette[tone || "primary"];
  return (
    <div className="panel" style={{position:"relative", padding:"14px 16px"}}>
      <span style={{position:"absolute", left:0, top:0, bottom:0, width:3, background:c.strip, borderRadius:"12px 0 0 12px"}}/>
      <div style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>{label}</div>
      <div style={{fontSize:24, fontWeight:700, color:c.fg, fontVariantNumeric:"tabular-nums", lineHeight:1.2, marginTop:4}}>{value}</div>
      {sub && <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:2}}>{sub}</div>}
    </div>
  );
};

/* Money formatting helpers — keep concise across reports. */
const fmtMoney = (n, currency = "EUR") => {
  const sym = currency === "EUR" ? "€" : currency === "USD" ? "$" : currency === "GBP" ? "£" : currency + " ";
  if (Math.abs(n) >= 1_000_000) return `${sym}${(n/1_000_000).toFixed(2)}M`;
  if (Math.abs(n) >= 1_000) return `${sym}${(n/1_000).toFixed(1)}K`;
  return `${sym}${Math.round(n).toLocaleString()}`;
};
const fmtCount = (n) => n.toLocaleString();
/* NULL RENDERS "—". Every percentage on this page went through here, and
   `n.toFixed` on a null throws — so a null had to become 0 somewhere upstream,
   which is how "not settled yet" turned into "0.00%". */
const fmtPct = (n, decimals = 1) => (n == null || isNaN(n) ? "—" : `${Number(n).toFixed(decimals)}%`);

/* ---------- TAB · FINANCIAL REPORT ---------- */
/* Replaced the previous Money flow tab.  Total Deposits, Total
   Withdrawals, Profit (= Deposits − Withdrawals), with breakdowns by
   Brand / Payment method / Provider / Transaction type / Status. Every
   table honors the unified filter row at the top of Reports. */
/* Fee model (illustrative, single source of truth across Financial and
   Fees tabs).  Per-method effective deposit / withdrawal fee parsed from
   the Methods catalog where available, otherwise falls back to a
   sensible average.  Engine produces:
     fee(tx)        — total fee on this transaction
     pspCost(tx)    — what the PSP itself charged
     margin(tx)    = fee − pspCost (what the operator actually keeps)   */
/* THE FEE ENGINE WAS INVENTED, AND EVERY FIGURE ON THE FEES REPORT CAME OUT OF
   IT. `REPORT_METHOD_FEES` gave each method a deposit rate of
   `0.015 + (i % 5) * 0.0025`, a withdrawal rate, a fixed component and a "PSP
   cost" — all from the method's INDEX IN THE ARRAY. `txFee`, `txPspCost` and
   `txMargin` then multiplied real-looking amounts by those, so the report said
   what the operator keeps on every payment, computed from nothing.

   029 exists precisely because of this, and its comment is the argument:

     Only RATES existed: skin_payment_methods.fee_pct and payment_providers'
     fee_deposit_pct / fee_withdrawal_pct. A rate is not a fee. The rate can
     change, the provider can apply a minimum, and a report that recomputes a
     historical fee from today's rate produces a number that was never charged.

   So `fee_amount` is read, never derived. It is NULL until the PSP reports it
   on settlement, and null stays null all the way to the cell — a report that
   turns "not settled yet" into 0.00 understates cost silently.

   PSP COST AND MARGIN HAVE NO SOURCE. `fee_amount` is what the payment cost;
   there is no column for what the PSP charged the platform underneath it, so
   the split between the two — which is the whole of "what the operator keeps"
   — cannot be computed. Those panels say so rather than showing a difference
   between one real number and one invented one.
   <!-- SUGGESTION: to report margin, deposit_requests / withdrawal_requests need a psp_cost_amount beside fee_amount — what the provider charged the platform, as opposed to what was charged to the payment. Today only the second exists, so fee revenue is reportable and margin is not. --> */
const txFee = (t) => (t && t.fee_amount != null ? Number(t.fee_amount) : null);
/* Both null, deliberately: see above. Kept as functions so the call sites keep
   naming what they wanted, and so a future column changes one line. */
const txPspCost = () => null;
const txMargin = () => null;

const ReportsFinancial = ({ brand, scoped, brandLocked, currency }) => {
  const T = window.T || ((k, fb) => fb || k);

  const isDeposit    = (t) => t.type === "Deposit";
  const isWithdrawal = (t) => t.type === "Withdrawal";
  const isBalanced   = (t) => t.status === "balanced" || t.status === "completed" || t.status === "approved";

  // Only settled transactions count toward the financial totals — pending
  // and to-confirm rows are not money on the books yet.
  const settled = scoped.filter(isBalanced);
  const totalDep = settled.filter(isDeposit).reduce((a, t) => a + t.amount, 0);
  const totalWd  = settled.filter(isWithdrawal).reduce((a, t) => a + t.amount, 0);
  const profit   = totalDep - totalWd;
  const totalDepN = settled.filter(isDeposit).length;
  const totalWdN  = settled.filter(isWithdrawal).length;
  // Fees collected (operator side) on every settled transaction in scope.
  /* SUM THE KNOWN, COUNT THE UNKNOWN. `fee_amount` is null until the PSP
     reports it, and `a + null` is `a` — so an unsettled fee would silently
     vanish into a total that looks complete. The count of missing rows travels
     with the figure so the panel can say the total is partial. */
  const feeSum = (rows) => {
    let total = 0, missing = 0;
    rows.forEach(t => { const f = txFee(t); if (f == null) missing++; else total += f; });
    return { total, missing };
  };
  const feesTotalR = feeSum(settled);
  const feesDepR   = feeSum(settled.filter(isDeposit));
  const feesWdR    = feeSum(settled.filter(isWithdrawal));
  const feesTotal = feesTotalR.total;
  const feesDep   = feesDepR.total;
  const feesWd    = feesWdR.total;
  /* No column for what the PSP charged the platform, so this is not a number
     this build can produce. Null flows to the panel, which says so. */
  const pspCostTotal = null;
  const marginTotal  = +(feesTotal - pspCostTotal).toFixed(2);

  // Group helper — collapses `settled` by an arbitrary key + label fn.
  const groupBy = (keyFn, labelFn) => {
    const map = {};
    for (const t of settled) {
      const k = keyFn(t); if (!k) continue;
      if (!map[k]) map[k] = { key:k, label:labelFn(t), dep:0, depAmt:0, wd:0, wdAmt:0 };
      if (isDeposit(t))        { map[k].dep += 1; map[k].depAmt += t.amount; }
      else if (isWithdrawal(t)){ map[k].wd  += 1; map[k].wdAmt  += t.amount; }
    }
    return Object.values(map).map(r => ({ ...r, profit: r.depAmt - r.wdAmt }))
      .sort((a, b) => b.profit - a.profit);
  };

  const byBrand    = groupBy(t => t.brand,         t => t.brand_name  || t.brand);
  const byMethod   = groupBy(t => t.method,        t => t.method_name || t.method);
  const byProvider = groupBy(t => t.provider,      t => (window.lookupMethodProvider ? null : null) || t.provider);
  const byType     = groupBy(t => t.type,          t => t.type);

  // Profit by status — every status in the lifecycle, not just settled.
  // Approved is folded into Balanced because in this engine an approved
  // transaction has already been passed to the PSP and resolved as
  // Balanced or Failed.
  const STATUS_LABELS = {
    balanced:"Balanced", approved:"Balanced", completed:"Balanced",
    to_confirm:"To-Confirm", review:"To-Confirm",
    pending:"Pending", created:"Created",
    declined:"Failed", failed:"Failed", rejected:"Failed", errored:"Failed",
  };
  const STATUS_GROUP_KEY = (s) => {
    if (s === "balanced" || s === "approved" || s === "completed") return "balanced";
    if (s === "to_confirm" || s === "review") return "to_confirm";
    if (s === "declined" || s === "failed" || s === "rejected" || s === "errored") return "failed";
    return s || "unknown";
  };
  const byStatusAll = (() => {
    const map = {};
    for (const t of scoped) {
      const gk = STATUS_GROUP_KEY(t.status);
      if (!map[gk]) map[gk] = { key: gk, label: STATUS_LABELS[gk] || gk, count:0, dep:0, depAmt:0, wd:0, wdAmt:0 };
      const row = map[gk];
      row.count += 1;
      if (isDeposit(t))        { row.dep += 1; row.depAmt += t.amount; }
      else if (isWithdrawal(t)){ row.wd  += 1; row.wdAmt  += t.amount; }
    }
    const order = ["balanced","to_confirm","pending","created","failed"];
    return Object.values(map).sort((a, b) => {
      const ai = order.indexOf(a.key); const bi = order.indexOf(b.key);
      return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
    });
  })();

  // Pretty provider names from the global PSP catalogue.
  const pspNameById = (() => {
    const all = (window.getAllProviders && window.getAllProviders()) || [];
    return Object.fromEntries(all.map(p => [p.id, p.name]));
  })();
  byProvider.forEach(r => { r.label = pspNameById[r.key] || r.label || r.key; });

  const moneyTone = (v) => v >= 0 ? "var(--ok-700)" : "var(--err-700)";

  const Table = ({ title, desc, scopeWord, rows, exportName, keyLabel }) => (
    <ReportCard
      title={title}
      desc={desc}
      exportFilename={exportName}
      exportRows={() => rows.map(r => ({
        [keyLabel]: r.label,
        deposit_count: r.dep, deposit_amount: Math.round(r.depAmt),
        withdrawal_count: r.wd, withdrawal_amount: Math.round(r.wdAmt),
        profit: Math.round(r.profit),
      }))}
      exportCols={[
        {key:keyLabel,label:keyLabel},
        {key:"deposit_count",label:"deposit_count"},{key:"deposit_amount",label:"deposit_amount"},
        {key:"withdrawal_count",label:"withdrawal_count"},{key:"withdrawal_amount",label:"withdrawal_amount"},
        {key:"profit",label:"profit"},
      ]}>
      <table className="data-table" style={{width:"100%"}}>
        <thead>
          <tr>
            <th>{scopeWord}</th>
            <th style={{textAlign:"right"}}>Deposits</th>
            <th style={{textAlign:"right"}}>Withdrawals</th>
            <th style={{textAlign:"right"}}>Profit</th>
          </tr>
        </thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan={4} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No settled transactions in scope.</td></tr>}
          {rows.map(r => (
            <tr key={r.key}>
              <td style={{fontWeight:600}}>{r.label}</td>
              <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(r.depAmt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{r.dep}</span></td>
              <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(r.wdAmt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{r.wd}</span></td>
              <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums", color: moneyTone(r.profit)}}>{fmtMoney(r.profit, currency)}</td>
            </tr>
          ))}
        </tbody>
        {rows.length > 0 && (() => {
          const tDep = rows.reduce((a, r) => a + r.depAmt, 0);
          const tWd  = rows.reduce((a, r) => a + r.wdAmt, 0);
          const tDepN = rows.reduce((a, r) => a + r.dep, 0);
          const tWdN  = rows.reduce((a, r) => a + r.wd, 0);
          return (
            <tfoot>
              <tr style={{borderTop:"2px solid var(--border-default)", background:"var(--n-25)"}}>
                <td style={{fontWeight:700, padding:"10px 8px"}}>Total · {rows.length} {scopeWord.toLowerCase()}{rows.length===1?"":"s"}</td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(tDep, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{tDepN}</span></td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(tWd, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{tWdN}</span></td>
                <td style={{textAlign:"right", fontWeight:800, fontVariantNumeric:"tabular-nums", color: moneyTone(tDep - tWd)}}>{fmtMoney(tDep - tWd, currency)}</td>
              </tr>
            </tfoot>
          );
        })()}
      </table>
    </ReportCard>
  );

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      {/* Headline KPIs — total deposits, total withdrawals, profit, fees. */}
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr 1fr", gap:14}}>
        <KpiCard tone="primary"
          label="Total deposits"
          value={fmtMoney(totalDep, currency)}
          sub={`${fmtCount(totalDepN)} settled deposit${totalDepN===1?"":"s"}`}/>
        <KpiCard tone="warn"
          label="Total withdrawals"
          value={fmtMoney(totalWd, currency)}
          sub={`${fmtCount(totalWdN)} settled withdrawal${totalWdN===1?"":"s"}`}/>
        <KpiCard tone={profit >= 0 ? "primary" : "warn"}
          label="Profit"
          value={fmtMoney(profit, currency)}
          sub="Deposits − Withdrawals"/>
        <KpiCard tone="primary"
          label="Fees collected"
          value={fmtMoney(feesTotal, currency)}
          sub={`${fmtMoney(feesDep, currency)} deposits · ${fmtMoney(feesWd, currency)} withdrawals`}/>
      </div>

      <Table title="Profit by brand"
        desc="Total deposits, withdrawals and profit per brand in the active scope. Sorted by profit desc; the footer rolls up every brand in scope."
        scopeWord="Brand" rows={byBrand} exportName="financial-by-brand" keyLabel="brand"/>

      <Table title="Profit by payment method"
        desc="Same picture, sliced by the payment instrument the player chose (Visa, Mastercard, Apple Pay, Bank wire…)."
        scopeWord="Method" rows={byMethod} exportName="financial-by-method" keyLabel="method"/>

      <Table title="Profit by provider"
        desc="Sliced by the PSP that actually processed the transaction after the cascade ran."
        scopeWord="Provider" rows={byProvider} exportName="financial-by-provider" keyLabel="provider"/>

      <Table title="Profit by transaction type"
        desc="Deposits and withdrawals as separate rows. Reads as a double-check of the headline KPI."
        scopeWord="Type" rows={byType} exportName="financial-by-type" keyLabel="type"/>

      <ReportCard
        title="Profit by status — all statuses"
        desc="Every status in the lifecycle: Balanced (= Approved · the PSP settled the transaction), Pending (sent to the PSP, awaiting webhook), To-Confirm (paused for manual operator approval), Failed (declined / rejected / errored at any stage). Approved is folded into Balanced because in this engine an approved transaction has already been passed to the PSP and resolved as Balanced or Failed."
        exportFilename="financial-by-status"
        exportRows={() => byStatusAll.map(s => ({ status:s.label, count:s.count, deposit_count:s.dep, deposit_amount:Math.round(s.depAmt), withdrawal_count:s.wd, withdrawal_amount:Math.round(s.wdAmt), net: Math.round(s.depAmt - s.wdAmt) }))}
        exportCols={[
          {key:"status",label:"status"},
          {key:"count",label:"count"},
          {key:"deposit_count",label:"deposit_count"},{key:"deposit_amount",label:"deposit_amount"},
          {key:"withdrawal_count",label:"withdrawal_count"},{key:"withdrawal_amount",label:"withdrawal_amount"},
          {key:"net",label:"net"},
        ]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead>
            <tr>
              <th>Status</th>
              <th style={{textAlign:"right"}}>Tx</th>
              <th style={{textAlign:"right"}}>Deposits</th>
              <th style={{textAlign:"right"}}>Withdrawals</th>
              <th style={{textAlign:"right"}}>Net</th>
            </tr>
          </thead>
          <tbody>
            {byStatusAll.length === 0 && <tr><td colSpan={5} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No transactions in this window.</td></tr>}
            {byStatusAll.map(s => (
              <tr key={s.key}>
                <td style={{fontWeight:600}}>{s.label}{s.key === "balanced" && <span style={{marginLeft:6, fontSize:10.5, color:"var(--text-tertiary)"}}>(includes approved)</span>}</td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtCount(s.count)}</td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(s.depAmt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{s.dep}</span></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(s.wdAmt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{s.wd}</span></td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums", color: moneyTone(s.depAmt - s.wdAmt)}}>{fmtMoney(s.depAmt - s.wdAmt, currency)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </ReportCard>
    </div>
  );
};

/* ---------- TAB 3 · CONVERSION ---------- */

const ReportsConversion = ({ brand, scoped, days, windowStart, windowEnd }) => {
  // Approval rate trend per day.
  const trend = (() => {
    const labels = [];
    const apv = new Array(days).fill(0);
    const dec = new Array(days).fill(0);
    for (let i = 0; i < days; i++) {
      const d = new Date(windowEnd - (days - 1 - i) * 86400_000);
      labels.push(d.toLocaleDateString("en-GB", { day:"2-digit", month:"short" }));
    }
    for (const t of scoped) {
      const idx = Math.floor((t.created_at - windowStart) / 86400_000);
      if (idx < 0 || idx >= days) continue;
      if (t.status === "balanced" || t.status === "approved") apv[idx] += 1;
      else if (t.status === "declined" || t.status === "failed" || t.status === "rejected") dec[idx] += 1;
    }
    const rates = apv.map((a, i) => (a + dec[i]) ? (a / (a + dec[i])) * 100 : 0);
    return { labels, rates, apv, dec };
  })();

  // Methods ranked by approval rate.
  const methodRanking = (() => {
    const map = {};
    for (const t of scoped) {
      if (!map[t.method]) map[t.method] = { id:t.method, name:t.method_name, color:t.method_color, ok:0, bad:0 };
      if (t.status === "balanced" || t.status === "approved") map[t.method].ok += 1;
      else if (t.status === "declined" || t.status === "failed" || t.status === "rejected") map[t.method].bad += 1;
    }
    return Object.values(map)
      .map(m => ({ ...m, total: m.ok + m.bad, rate: (m.ok + m.bad) ? m.ok / (m.ok + m.bad) * 100 : 0 }))
      .filter(m => m.total >= 2)
      .sort((a, b) => b.rate - a.rate);
  })();

  /* DECLINE REASONS, from the requests themselves. 029 added
     `payment_decline_reasons` — a coded list with a CATEGORY (player /
     provider / risk / limit / technical), which is the split a decline chart is
     actually asking about: a player mistyping a card is not a provider outage.
     `deposit_requests.decline_reason` is the free-text field; the coded one is
     what can be grouped. Counted over the rows in scope rather than taken from
     a fixed table, so a reason nobody has hit is not shown as a category. */
  const declineReasons = (() => {
    const by = {};
    scoped.forEach(t => {
      const raw = t._raw || {};
      const code = raw.decline_reason_code || raw.decline_reason;
      if (!code) return;
      const k = String(code);
      by[k] = by[k] || { code: k, label: k, count: 0 };
      by[k].count += 1;
    });
    const rows = Object.values(by).sort((a, b) => b.count - a.count);
    const total = rows.reduce((a, r) => a + r.count, 0);
    return rows.map(r => ({ ...r, pct: total ? +((r.count / total) * 100).toFixed(1) : 0 }));
  })();
  /* PSP performance, computed from the same rows rather than read from a
     fixed table: approval rate and volume per provider. */
  const pspPerf = (() => {
    const by = {};
    scoped.forEach(t => {
      const name = t.provider_name || "—";
      const r = by[name] || (by[name] = { psp: name, total: 0, ok: 0, volume: 0 });
      r.total += 1;
      r.volume += t.amount;
      if (t.status === "approved" || t.status === "balanced" || t.status === "paid") r.ok += 1;
    });
    return Object.values(by)
      .map(r => ({ ...r, rate: r.total ? +((r.ok / r.total) * 100).toFixed(1) : null }))
      .sort((a, b) => b.volume - a.volume);
  })();

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      <ReportCard
        title="Approval rate trend"
        desc="Share of decided transactions that ended in Approved or Balanced — per day"
        exportFilename={`reports-approval-trend-${days}d`}
        exportRows={() => trend.labels.map((d, i) => ({ date:d, approved:trend.apv[i], failed:trend.dec[i], approval_rate_pct: +trend.rates[i].toFixed(2) }))}
        exportCols={[{key:"date",label:"date"},{key:"approved",label:"approved"},{key:"failed",label:"failed"},{key:"approval_rate_pct",label:"approval_rate_pct"}]}>
        {/* Inline bar viz of approval rate per day. */}
        <div style={{display:"flex", alignItems:"flex-end", gap:2, height:110}}>
          {trend.rates.map((r, i) => {
            const tone = r >= 95 ? "var(--ok-500)" : r >= 85 ? "var(--warn-500)" : "var(--err-500)";
            return (
              <div key={i} title={`${trend.labels[i]} — ${r.toFixed(1)}%`} style={{flex:1, minWidth:0, display:"flex", flexDirection:"column", justifyContent:"flex-end", alignItems:"center", gap:3}}>
                <div style={{fontSize:9, color:"var(--text-tertiary)", fontVariantNumeric:"tabular-nums", fontWeight:600}}>{r > 0 ? r.toFixed(0) : ""}</div>
                <div style={{width:"100%", height:`${r}%`, background:tone, opacity:.85, borderRadius:"3px 3px 0 0", minHeight: r > 0 ? 2 : 0}}/>
              </div>
            );
          })}
        </div>
        <div style={{display:"flex", justifyContent:"space-between", fontSize:10, color:"var(--text-tertiary)", marginTop:4}}>
          <span>{trend.labels[0]}</span>
          <span>{trend.labels[Math.floor(days/2)]}</span>
          <span>{trend.labels[days-1]}</span>
        </div>
      </ReportCard>

      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
        <ReportCard
          title="Top decline reasons"
          desc="Why transactions failed in this window — sum across providers"
          exportFilename={`reports-declines-${days}d`}
          exportRows={() => declineReasons.map(r => ({ code:r.code, label:r.label, count:r.count, pct:r.pct }))}
          exportCols={[{key:"code",label:"code"},{key:"label",label:"label"},{key:"count",label:"count"},{key:"pct",label:"pct"}]}>
          <div style={{display:"flex", flexDirection:"column", gap:6}}>
            {declineReasons.map((r, i) => {
              const palette = ["var(--err-500)","var(--warn-500)","var(--purple-500)","var(--info-500)","var(--p-500)","var(--text-tertiary)"];
              const pct = (r.pct || 0);
              return (
                <div key={r.code}>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                    <span style={{display:"flex", alignItems:"center", gap:6}}>
                      <span style={{width:8, height:8, borderRadius:2, background:palette[i % palette.length]}}/>
                      <span style={{fontWeight:550}}>{r.label}</span>
                    </span>
                    <span style={{fontVariantNumeric:"tabular-nums", fontWeight:600}}>{r.count}<span style={{color:"var(--text-tertiary)", fontWeight:500, marginLeft:6, fontSize:11}}>{pct}%</span></span>
                  </div>
                  <div style={{height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                    <div style={{height:"100%", width:`${pct}%`, background:palette[i % palette.length], opacity:.85}}/>
                  </div>
                </div>
              );
            })}
          </div>
        </ReportCard>

        <ReportCard
          title="Methods ranked by approval rate"
          desc="Best-performing rails on top — sample of two or more transactions"
          exportFilename={`reports-method-approval-${days}d`}
          exportRows={() => methodRanking.map(m => ({ method:m.name, approved:m.ok, failed:m.bad, total:m.total, approval_rate_pct: +m.rate.toFixed(2) }))}
          exportCols={[{key:"method",label:"method"},{key:"approved",label:"approved"},{key:"failed",label:"failed"},{key:"total",label:"total"},{key:"approval_rate_pct",label:"approval_rate_pct"}]}>
          <div style={{display:"flex", flexDirection:"column", gap:7}}>
            {methodRanking.length === 0 && <div style={{fontSize:12, color:"var(--text-tertiary)"}}>Not enough transactions in this window to rank methods.</div>}
            {methodRanking.map(m => {
              const tone = m.rate >= 95 ? "var(--ok-600)" : m.rate >= 85 ? "var(--warn-600)" : "var(--err-600)";
              return (
                <div key={m.id}>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                    <span style={{display:"flex", alignItems:"center", gap:6}}>
                      <span style={{width:8, height:8, borderRadius:2, background:m.color}}/>
                      <span style={{fontWeight:550}}>{m.name}</span>
                    </span>
                    <span style={{fontVariantNumeric:"tabular-nums", color:tone, fontWeight:600}}>{fmtPct(m.rate)}<span style={{color:"var(--text-tertiary)", fontWeight:500, marginLeft:6, fontSize:11}}>({m.ok}/{m.total})</span></span>
                  </div>
                  <div style={{height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                    <div style={{height:"100%", width:`${m.rate}%`, background:tone}}/>
                  </div>
                </div>
              );
            })}
          </div>
        </ReportCard>
      </div>

      <ReportCard
        title="PSP performance"
        desc="Success rate, latency and timeout rate per provider. Pulled from the live PSP health feed."
        exportFilename={`reports-psp-perf-${days}d`}
        exportRows={() => pspPerf.map(p => ({ provider:p.name, methods:(p.methods || []).join("·"), approval_rate_pct: p.approval, p95_ms: p.p95_ms, volume_30d: p.volume_30d, paused: p.paused ? "yes" : "no" }))}
        exportCols={[{key:"provider",label:"provider"},{key:"methods",label:"methods"},{key:"approval_rate_pct",label:"approval_rate_pct"},{key:"p95_ms",label:"p95_ms"},{key:"volume_30d",label:"volume_30d"},{key:"paused",label:"paused"}]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead><tr><th>Provider</th><th>Methods</th><th style={{textAlign:"right"}}>Approval</th><th style={{textAlign:"right"}}>p95 latency</th><th style={{textAlign:"right"}}>30d volume</th><th>Status</th></tr></thead>
          <tbody>
            {pspPerf.map(p => {
              const tone = p.approval >= 95 ? "var(--ok-700)" : p.approval >= 85 ? "var(--warn-700)" : "var(--err-700)";
              return (
                <tr key={p.id}>
                  <td style={{fontWeight:600}}>{p.name}</td>
                  <td style={{fontSize:11, color:"var(--text-tertiary)"}}>{(p.methods || []).join(" · ")}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600, color:tone}}>{fmtPct(p.approval, 1)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{p.p95_ms} ms</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(p.volume_30d)}</td>
                  <td>{p.paused ? <span className="chip chip--neutral" style={{fontSize:10}}>PAUSED</span> : <span className="chip chip--ok" style={{fontSize:10}}>ONLINE</span>}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </ReportCard>
    </div>
  );
};

/* ---------- TAB 4 · BRANDS & GEO ---------- */

/* Geo tab — country breakdowns only. Brand-level totals live in the
   Financial Report tab; keeping both here was duplication. */
const ReportsGeoBrand = ({ brand, scoped, currency }) => {
  // Per-country volume (top 10 used for the bar list).
  const byCountry = (() => {
    const map = {};
    for (const t of scoped) {
      const k = t.country || "??";
      if (!map[k]) map[k] = { code:k, count:0, amt:0, depAmt:0, wdAmt:0 };
      map[k].count += 1;
      map[k].amt += t.amount;
      if (t.type === "Deposit") map[k].depAmt += t.amount;
      else map[k].wdAmt += t.amount;
    }
    return Object.values(map).sort((a,b) => b.amt - a.amt).slice(0, 10);
  })();

  // Per-country full totals — deposits / withdrawals / net for every
  // country in the window. Sorted by net descending so positive flows
  // surface first; negative (cash-out heavy) drop to the bottom.
  const byCountryFull = (() => {
    const map = {};
    for (const t of scoped) {
      const k = t.country || "??";
      if (!map[k]) map[k] = { code:k, dep_count:0, dep_amt:0, wd_count:0, wd_amt:0 };
      if (t.type === "Deposit")        { map[k].dep_count += 1; map[k].dep_amt += t.amount; }
      else if (t.type === "Withdrawal"){ map[k].wd_count  += 1; map[k].wd_amt  += t.amount; }
    }
    return Object.values(map)
      .map(c => ({ ...c, net: c.dep_amt - c.wd_amt }))
      .sort((a, b) => b.net - a.net);
  })();

  // Avg ticket by country (deposits).
  const avgTicket = (() => {
    const map = {};
    for (const t of scoped) {
      if (t.type !== "Deposit") continue;
      const k = t.country || "??";
      if (!map[k]) map[k] = { code:k, count:0, amt:0 };
      map[k].count += 1;
      map[k].amt += t.amount;
    }
    return Object.values(map)
      .filter(c => c.count > 0)
      .map(c => ({ ...c, avg: c.amt / c.count }))
      .sort((a,b) => b.avg - a.avg)
      .slice(0, 8);
  })();

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
        <ReportCard
          title="Top countries"
          desc="By total transacted volume — deposits + withdrawals"
          exportFilename="reports-by-country"
          exportRows={() => byCountry.map(c => ({ country:c.code, transactions:c.count, deposit_volume:Math.round(c.depAmt), withdrawal_volume:Math.round(c.wdAmt), total_volume:Math.round(c.amt) }))}
          exportCols={[{key:"country",label:"country"},{key:"transactions",label:"transactions"},{key:"deposit_volume",label:"deposit_volume"},{key:"withdrawal_volume",label:"withdrawal_volume"},{key:"total_volume",label:"total_volume"}]}>
          <div style={{display:"flex", flexDirection:"column", gap:7}}>
            {byCountry.length === 0 && <div style={{fontSize:12, color:"var(--text-tertiary)"}}>No transactions in this window.</div>}
            {byCountry.map(c => {
              const max = byCountry[0]?.amt || 1;
              const pct = (c.amt / max) * 100;
              return (
                <div key={c.code}>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                    <span style={{display:"flex", alignItems:"center", gap:6}}>
                      <span style={{fontWeight:700, fontFamily:"var(--font-mono)", color:"var(--text-secondary)", fontSize:11}}>{c.code}</span>
                      <span style={{color:"var(--text-tertiary)", fontSize:11}}>{c.count} tx</span>
                    </span>
                    <span style={{fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtMoney(c.amt, currency)}</span>
                  </div>
                  <div style={{height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                    <div style={{height:"100%", width:`${pct}%`, background:"var(--primary, #1e40af)"}}/>
                  </div>
                </div>
              );
            })}
          </div>
        </ReportCard>

        <ReportCard
          title="Avg deposit ticket by country"
          desc="Mean deposit size per country — flags VIP-heavy geos"
          exportFilename="reports-avg-ticket-by-country"
          exportRows={() => avgTicket.map(c => ({ country:c.code, deposit_count:c.count, total_amount:Math.round(c.amt), avg_ticket: +c.avg.toFixed(2) }))}
          exportCols={[{key:"country",label:"country"},{key:"deposit_count",label:"deposit_count"},{key:"total_amount",label:"total_amount"},{key:"avg_ticket",label:"avg_ticket"}]}>
          <div style={{display:"flex", flexDirection:"column", gap:7}}>
            {avgTicket.length === 0 && <div style={{fontSize:12, color:"var(--text-tertiary)"}}>No deposits in this window.</div>}
            {avgTicket.map(c => {
              const max = avgTicket[0]?.avg || 1;
              const pct = (c.avg / max) * 100;
              return (
                <div key={c.code}>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                    <span style={{display:"flex", alignItems:"center", gap:6}}>
                      <span style={{fontWeight:700, fontFamily:"var(--font-mono)", color:"var(--text-secondary)", fontSize:11}}>{c.code}</span>
                      <span style={{color:"var(--text-tertiary)", fontSize:11}}>{c.count} dep</span>
                    </span>
                    <span style={{fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtMoney(c.avg, currency)}</span>
                  </div>
                  <div style={{height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                    <div style={{height:"100%", width:`${pct}%`, background:"var(--g-400)"}}/>
                  </div>
                </div>
              );
            })}
          </div>
        </ReportCard>
      </div>

      <ReportCard
        title="Deposits, withdrawals & net by country"
        desc="Full country breakdown of money in / out / net, sorted by net so positive flows are at the top and cash-out heavy countries fall to the bottom. The footer totals the rows in scope."
        exportFilename="reports-net-by-country"
        exportRows={() => byCountryFull.map(c => ({ country:c.code, deposits:c.dep_count, deposit_amount:Math.round(c.dep_amt), withdrawals:c.wd_count, withdrawal_amount:Math.round(c.wd_amt), net:Math.round(c.net) }))}
        exportCols={[{key:"country",label:"country"},{key:"deposits",label:"deposits"},{key:"deposit_amount",label:"deposit_amount"},{key:"withdrawals",label:"withdrawals"},{key:"withdrawal_amount",label:"withdrawal_amount"},{key:"net",label:"net"}]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead>
            <tr>
              <th>Country</th>
              <th style={{textAlign:"right"}}>Deposits</th>
              <th style={{textAlign:"right"}}>Withdrawals</th>
              <th style={{textAlign:"right"}}>Net</th>
            </tr>
          </thead>
          <tbody>
            {byCountryFull.length === 0 && <tr><td colSpan={4} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No transactions in this window.</td></tr>}
            {byCountryFull.map(c => (
              <tr key={c.code}>
                <td><span style={{fontWeight:700, fontFamily:"var(--font-mono)", color:"var(--text-secondary)"}}>{c.code}</span></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(c.dep_amt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{c.dep_count}</span></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(c.wd_amt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{c.wd_count}</span></td>
                <td style={{textAlign:"right", fontWeight:600, fontVariantNumeric:"tabular-nums", color: c.net >= 0 ? "var(--ok-700)" : "var(--err-700)"}}>{fmtMoney(c.net, currency)}</td>
              </tr>
            ))}
          </tbody>
          {byCountryFull.length > 0 && (() => {
            const totDep = byCountryFull.reduce((a, c) => a + c.dep_amt, 0);
            const totWd  = byCountryFull.reduce((a, c) => a + c.wd_amt, 0);
            const totDepN = byCountryFull.reduce((a, c) => a + c.dep_count, 0);
            const totWdN  = byCountryFull.reduce((a, c) => a + c.wd_count, 0);
            return (
              <tfoot>
                <tr style={{borderTop:"2px solid var(--border-default)", background:"var(--n-25)"}}>
                  <td style={{fontWeight:700, padding:"10px 8px"}}>Total · {byCountryFull.length} countr{byCountryFull.length===1?"y":"ies"}</td>
                  <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(totDep, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{totDepN}</span></td>
                  <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(totWd, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{totWdN}</span></td>
                  <td style={{textAlign:"right", fontWeight:800, fontVariantNumeric:"tabular-nums", color: (totDep - totWd) >= 0 ? "var(--ok-700)" : "var(--err-700)"}}>{fmtMoney(totDep - totWd, currency)}</td>
                </tr>
              </tfoot>
            );
          })()}
        </table>
      </ReportCard>
    </div>
  );
};

/* ---------- TAB 5 · OPERATIONS ---------- */

const ReportsOps = ({ brand, scoped, currency }) => {
  /* `activity_logs` is the real audit trail — 039 writes a row for every
     settlement decision, which is exactly what this panel lists. */
  const actFeed = useHrsFetch(() => window.sb.list("activityLogs", { limit: 200 }), []);
  const activity = (actFeed.data || []);
  const pspFeed = useHrsFetch(() => window.sb.list("paymentProviders", { limit: 300 }), []);
  const pspProfiles = (pspFeed.data || []);

  // To-Confirm queue snapshot.
  const toConfirm = scoped.filter(t => t.status === "to_confirm" || t.status === "review");
  const oldestAgeMin = toConfirm.length
    ? Math.floor((Date.now() - Math.min(...toConfirm.map(t => t.created_at))) / 60_000)
    : 0;
  const totalAtRisk = toConfirm.reduce((a, t) => a + t.amount, 0);

  // Operator actions in scoped's activity window.
  const opByKind = (() => {
    const map = { approve:0, reject:0, psp_login:0, method_edit:0, note:0 };
    for (const a of activity) {
      const k = a.action?.kind;
      if (map[k] != null) map[k] += 1;
    }
    return map;
  })();

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:14}}>
        <KpiCard tone="warn"    label="To-Confirm queue" value={fmtCount(toConfirm.length)}     sub={`${fmtMoney(totalAtRisk, currency)} held`}/>
        <KpiCard tone="neutral" label="Oldest in queue"  value={oldestAgeMin > 0 ? `${oldestAgeMin}m` : "—"} sub="age of oldest unresolved"/>
        <KpiCard tone="primary" label="Operator actions" value={fmtCount(opByKind.approve + opByKind.reject + opByKind.method_edit + opByKind.psp_login + opByKind.note)} sub={`${opByKind.approve} approve · ${opByKind.reject} reject`}/>
      </div>

      <ReportCard
        title="Operator actions by type"
        desc="What the back-office crew has been doing in this window"
        exportFilename="reports-ops-actions"
        exportRows={() => Object.entries(opByKind).map(([k, n]) => ({ action_kind:k, count:n }))}
        exportCols={[{key:"action_kind",label:"action_kind"},{key:"count",label:"count"}]}>
        <div style={{display:"flex", flexDirection:"column", gap:7}}>
          {Object.entries(opByKind).map(([k, n]) => {
            const max = Math.max(1, ...Object.values(opByKind));
            const pct = (n / max) * 100;
            const tone = k === "reject" ? "var(--err-600)" : k === "approve" ? "var(--ok-600)" : k === "method_edit" ? "var(--info-500)" : "var(--p-600)";
            const label = { approve:"Approvals", reject:"Rejections", psp_login:"PSP logins", method_edit:"Method edits", note:"Player notes" }[k] || k;
            return (
              <div key={k}>
                <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                  <span style={{fontWeight:550}}>{label}</span>
                  <span style={{fontVariantNumeric:"tabular-nums", fontWeight:600, color:tone}}>{fmtCount(n)}</span>
                </div>
                <div style={{height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                  <div style={{height:"100%", width:`${pct}%`, background:tone, opacity:.85}}/>
                </div>
              </div>
            );
          })}
        </div>
      </ReportCard>

      <ReportCard
        title="PSP liquidity status"
        desc="Wallet position vs floor/ceiling — flags top-up and sweep needs"
        exportFilename="reports-psp-liquidity"
        exportRows={() => pspProfiles.map(p => {
          const w = p.liquidity?.walletEur || 0, c = p.liquidity?.ceilingEur || 1, f = p.liquidity?.floorEur || 0;
          const status = w < f ? "TOP UP" : (w / c) * 100 >= (p.liquidity?.sweepPct || 80) ? "SWEEP" : "HEALTHY";
          return { provider:p.name, wallet_eur:w, floor_eur:f, ceiling_eur:c, status };
        })}
        exportCols={[{key:"provider",label:"provider"},{key:"wallet_eur",label:"wallet_eur"},{key:"floor_eur",label:"floor_eur"},{key:"ceiling_eur",label:"ceiling_eur"},{key:"status",label:"status"}]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead><tr><th>Provider</th><th style={{textAlign:"right"}}>Wallet</th><th style={{textAlign:"right"}}>Floor</th><th style={{textAlign:"right"}}>Ceiling</th><th>Status</th></tr></thead>
          <tbody>
            {pspProfiles.map(p => {
              const w = p.liquidity?.walletEur || 0, c = p.liquidity?.ceilingEur || 1, f = p.liquidity?.floorEur || 0;
              const pctCeil = (w / c) * 100;
              const status = w < f ? { label:"TOP UP",  bg:"var(--err-50)",  fg:"var(--err-700)" }
                          : pctCeil >= (p.liquidity?.sweepPct || 80) ? { label:"SWEEP",  bg:"var(--warn-50)", fg:"#92400e" }
                          :                                              { label:"HEALTHY", bg:"var(--ok-50)", fg:"var(--ok-700)" };
              return (
                <tr key={p.id}>
                  <td style={{fontWeight:600}}>{p.name}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(w, currency)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", color:"var(--text-tertiary)"}}>{fmtMoney(f, currency)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", color:"var(--text-tertiary)"}}>{fmtMoney(c, currency)}</td>
                  <td><span style={{fontSize:10, fontWeight:700, padding:"2px 7px", borderRadius:6, letterSpacing:".05em", background:status.bg, color:status.fg}}>{status.label}</span></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </ReportCard>

    </div>
  );
};

/* ---------- main Reports page ---------- */

/* ---------- TAB · PLAYERS ----------
   Player-centric breakdowns of the same scoped window. Three reports:
   most-active by transaction count, biggest depositors, biggest
   withdrawers. Each one exports to CSV. */

const ReportsPlayers = ({ brand, scoped, onOpenPlayer, currency }) => {
  // Each card starts collapsed to "Top N" and expands to the full list on
  // demand — a single shared cap value applied to every card so the four
  // tables stay aligned visually.
  const DEFAULT_CAP = 15;
  const [expandedCards, setExpandedCards] = useState({}); // { active, dep, wd, net }
  const isExpanded = (key) => !!expandedCards[key];
  const toggleExpand = (key) => setExpandedCards(s => ({ ...s, [key]: !s[key] }));

  // Aggregate per-player.
  const byPlayer = (() => {
    const map = {};
    for (const t of scoped) {
      const k = t.user_id || t.user || t.player_id;
      if (!k) continue;
      if (!map[k]) {
        map[k] = {
          id: k,
          name: t.player_name || t.player || t.user_name || k,
          email: t.player_email || t.email || "",
          country: t.country || "—",
          brand: t.brand_name || "",
          brand_id: t.brand,
          brand_short: t.brand_short || "",
          brand_color: t.brand_color || "var(--n-300)",
          tx_count: 0,
          dep_count: 0, dep_amt: 0,
          wd_count: 0,  wd_amt: 0,
          failed: 0, balanced: 0,
        };
      }
      const row = map[k];
      row.tx_count += 1;
      if (t.type === "Deposit")        { row.dep_count += 1; row.dep_amt += t.amount; }
      else if (t.type === "Withdrawal"){ row.wd_count  += 1; row.wd_amt  += t.amount; }
      const s = (t.status || "").toLowerCase();
      if (s === "balanced" || s === "approved") row.balanced += 1;
      else if (s === "declined" || s === "failed" || s === "rejected") row.failed += 1;
    }
    return Object.values(map).map(r => ({ ...r, net: r.dep_amt - r.wd_amt }));
  })();

  // Sorted slices — full lists; the UI caps display based on isExpanded(key).
  const allActive = [...byPlayer].sort((a, b) => b.tx_count - a.tx_count);
  const allDep    = [...byPlayer].filter(p => p.dep_amt > 0).sort((a, b) => b.dep_amt - a.dep_amt);
  const allWd     = [...byPlayer].filter(p => p.wd_amt  > 0).sort((a, b) => b.wd_amt  - a.wd_amt);
  const allNet    = [...byPlayer].sort((a, b) => b.net      - a.net);
  const take = (list, key) => isExpanded(key) ? list : list.slice(0, DEFAULT_CAP);

  // Totals across ALL players in scope (not just the top slice).
  const totals = {
    players: byPlayer.length,
    tx:      byPlayer.reduce((a, p) => a + p.tx_count, 0),
    depAmt:  byPlayer.reduce((a, p) => a + p.dep_amt, 0),
    depN:    byPlayer.reduce((a, p) => a + p.dep_count, 0),
    wdAmt:   byPlayer.reduce((a, p) => a + p.wd_amt, 0),
    wdN:     byPlayer.reduce((a, p) => a + p.wd_count, 0),
  };
  totals.net = totals.depAmt - totals.wdAmt;

  // Open a player in the back-office Player 360 view. We resolve the rich
  // player record from MOCK.PLAYERS_LIST so the 360 page has every field
  // (limits, notes, KYC) — the aggregated row from transactions only has
  // the fields we accumulated above.
  const openPlayer = (p) => {
    if (!onOpenPlayer) return;
    /* The aggregated row IS the record. It used to be looked up in a
       generated PLAYERS_LIST and fall back to a synthesised object when the id
       was not found — so opening a player could hand the detail screen a
       record assembled here rather than one that exists. */
    onOpenPlayer({ id: p.id, name: p.name, email: p.email, brand: p.brand_id,
                   brand_name: p.brand, brand_short: p.brand_short,
                   brand_color: p.brand_color, country: p.country });
  };

  const PlayerCell = ({ p }) => (
    <span
      onClick={() => openPlayer(p)}
      title="Open in Player 360"
      style={{display:"inline-flex", alignItems:"center", gap:8, cursor:"pointer"}}>
      <span style={{width:18, height:18, borderRadius:4, background:p.brand_color, color:"#fff", fontSize:9, fontWeight:700, display:"grid", placeItems:"center"}} title={p.brand}>{p.brand_short}</span>
      <span>
        <div style={{fontWeight:600, fontSize:12.5, color:"var(--p-700, #1e3a8a)", textDecoration:"underline", textDecorationColor:"transparent", textUnderlineOffset:2, transition:"text-decoration-color .12s"}}
          onMouseEnter={e => { e.currentTarget.style.textDecorationColor = "var(--p-500, #1e40af)"; }}
          onMouseLeave={e => { e.currentTarget.style.textDecorationColor = "transparent"; }}>
          {p.name}
        </div>
        <div style={{fontSize:10.5, color:"var(--text-tertiary)"}}>{p.country}{p.email ? ` · ${p.email}` : ""}</div>
      </span>
    </span>
  );

  // Footer toggle used at the bottom of every player table — shows
  // "Show all N" or "Collapse to top 15".
  const ExpandRow = ({ list, cardKey, cols }) => {
    if (list.length <= DEFAULT_CAP) return null;
    return (
      <tr>
        <td colSpan={cols} style={{padding:"6px 8px", background:"var(--n-25)", borderTop:"1px dashed var(--border-default)"}}>
          <button type="button" onClick={() => toggleExpand(cardKey)}
            style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 10px",
              border:"1px solid var(--border-default)", borderRadius:999, background:"#fff",
              color:"var(--p-700, #1e3a8a)", fontSize:11.5, fontWeight:700, cursor:"pointer"}}>
            {isExpanded(cardKey)
              ? <><Icon name="chevron_up" size={11}/> Collapse to top {DEFAULT_CAP}</>
              : <><Icon name="chevron_down" size={11}/> Show all {fmtCount(list.length)} players</>}
          </button>
        </td>
      </tr>
    );
  };

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      {/* Player totals — count of distinct players across every transaction
          in scope, plus the aggregate financial picture. */}
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr 1fr", gap:14}}>
        <KpiCard tone="primary"
          label="Players in scope"
          value={fmtCount(totals.players)}
          sub={`${fmtCount(totals.tx)} transactions`}/>
        <KpiCard tone="primary"
          label="Total deposits"
          value={fmtMoney(totals.depAmt, currency)}
          sub={`${fmtCount(totals.depN)} deposits`}/>
        <KpiCard tone="warn"
          label="Total withdrawals"
          value={fmtMoney(totals.wdAmt, currency)}
          sub={`${fmtCount(totals.wdN)} withdrawals`}/>
        <KpiCard tone={totals.net >= 0 ? "primary" : "warn"}
          label="Net (deposits − withdrawals)"
          value={fmtMoney(totals.net, currency)}
          sub={totals.net >= 0 ? "positive lifetime value" : "players are net-up"}/>
      </div>

      <ReportCard
        title="Most active players"
        desc="By total transaction count in window — flags whales, bots, and promo grinders. Click a name to open the player in Player 360."
        exportFilename="reports-top-active-players"
        exportRows={() => allActive.map(p => ({ player:p.name, email:p.email, brand:p.brand, country:p.country, transactions:p.tx_count, deposits:p.dep_count, withdrawals:p.wd_count, deposit_amount:Math.round(p.dep_amt), withdrawal_amount:Math.round(p.wd_amt), net:Math.round(p.net) }))}
        exportCols={[
          {key:"player",label:"player"},{key:"email",label:"email"},{key:"brand",label:"brand"},{key:"country",label:"country"},
          {key:"transactions",label:"transactions"},{key:"deposits",label:"deposits"},{key:"withdrawals",label:"withdrawals"},
          {key:"deposit_amount",label:"deposit_amount"},{key:"withdrawal_amount",label:"withdrawal_amount"},{key:"net",label:"net"},
        ]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead>
            <tr>
              <th>Player</th>
              <th style={{textAlign:"right"}}>Tx</th>
              <th style={{textAlign:"right"}}>Deposits</th>
              <th style={{textAlign:"right"}}>Withdrawals</th>
              <th style={{textAlign:"right"}}>Net</th>
            </tr>
          </thead>
          <tbody>
            {allActive.length === 0 && <tr><td colSpan={5} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No transactions in this window.</td></tr>}
            {take(allActive, "active").map(p => (
              <tr key={p.id}>
                <td><PlayerCell p={p}/></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtCount(p.tx_count)}</td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(p.dep_amt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{p.dep_count}</span></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(p.wd_amt, currency)} <span style={{color:"var(--text-tertiary)", fontSize:11, marginLeft:6}}>{p.wd_count}</span></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600, color: p.net >= 0 ? "var(--ok-700)" : "var(--err-700)"}}>{fmtMoney(p.net, currency)}</td>
              </tr>
            ))}
            <ExpandRow list={allActive} cardKey="active" cols={5}/>
          </tbody>
        </table>
      </ReportCard>

      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
        <ReportCard
          title="Biggest depositors"
          desc="Total deposit volume per player — VIP and acquisition signal. Click a name to open the player."
          exportFilename="reports-top-depositors"
          exportRows={() => allDep.map(p => ({ player:p.name, brand:p.brand, country:p.country, deposits:p.dep_count, deposit_amount:Math.round(p.dep_amt) }))}
          exportCols={[{key:"player",label:"player"},{key:"brand",label:"brand"},{key:"country",label:"country"},{key:"deposits",label:"deposits"},{key:"deposit_amount",label:"deposit_amount"}]}>
          <table className="data-table" style={{width:"100%"}}>
            <thead><tr><th>Player</th><th style={{textAlign:"right"}}>Deposits</th><th style={{textAlign:"right"}}>Amount</th></tr></thead>
            <tbody>
              {allDep.length === 0 && <tr><td colSpan={3} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No deposits in this window.</td></tr>}
              {take(allDep, "dep").map(p => (
                <tr key={p.id}>
                  <td><PlayerCell p={p}/></td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtCount(p.dep_count)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtMoney(p.dep_amt, currency)}</td>
                </tr>
              ))}
              <ExpandRow list={allDep} cardKey="dep" cols={3}/>
            </tbody>
          </table>
        </ReportCard>

        <ReportCard
          title="Biggest withdrawers"
          desc="Total withdrawal volume per player — payout-load and liquidity risk. Click a name to open the player."
          exportFilename="reports-top-withdrawers"
          exportRows={() => allWd.map(p => ({ player:p.name, brand:p.brand, country:p.country, withdrawals:p.wd_count, withdrawal_amount:Math.round(p.wd_amt) }))}
          exportCols={[{key:"player",label:"player"},{key:"brand",label:"brand"},{key:"country",label:"country"},{key:"withdrawals",label:"withdrawals"},{key:"withdrawal_amount",label:"withdrawal_amount"}]}>
          <table className="data-table" style={{width:"100%"}}>
            <thead><tr><th>Player</th><th style={{textAlign:"right"}}>Withdrawals</th><th style={{textAlign:"right"}}>Amount</th></tr></thead>
            <tbody>
              {allWd.length === 0 && <tr><td colSpan={3} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No withdrawals in this window.</td></tr>}
              {take(allWd, "wd").map(p => (
                <tr key={p.id}>
                  <td><PlayerCell p={p}/></td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtCount(p.wd_count)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600, color:"var(--err-700)"}}>{fmtMoney(p.wd_amt, currency)}</td>
                </tr>
              ))}
              <ExpandRow list={allWd} cardKey="wd" cols={3}/>
            </tbody>
          </table>
        </ReportCard>
      </div>

      <ReportCard
        title="Top net positions"
        desc="Players with the highest deposit-minus-withdrawal balance in window — best customers and biggest pay-out risks at a glance. Click a name to open the player."
        exportFilename="reports-top-net-players"
        exportRows={() => allNet.map(p => ({ player:p.name, brand:p.brand, country:p.country, deposit_amount:Math.round(p.dep_amt), withdrawal_amount:Math.round(p.wd_amt), net:Math.round(p.net) }))}
        exportCols={[{key:"player",label:"player"},{key:"brand",label:"brand"},{key:"country",label:"country"},{key:"deposit_amount",label:"deposit_amount"},{key:"withdrawal_amount",label:"withdrawal_amount"},{key:"net",label:"net"}]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead><tr><th>Player</th><th style={{textAlign:"right"}}>Deposits</th><th style={{textAlign:"right"}}>Withdrawals</th><th style={{textAlign:"right"}}>Net</th></tr></thead>
          <tbody>
            {allNet.length === 0 && <tr><td colSpan={4} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No transactions in this window.</td></tr>}
            {take(allNet, "net").map(p => (
              <tr key={p.id}>
                <td><PlayerCell p={p}/></td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(p.dep_amt, currency)}</td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(p.wd_amt, currency)}</td>
                <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:700, color: p.net >= 0 ? "var(--ok-700)" : "var(--err-700)"}}>{fmtMoney(p.net, currency)}</td>
              </tr>
            ))}
            <ExpandRow list={allNet} cardKey="net" cols={4}/>
          </tbody>
        </table>
      </ReportCard>
    </div>
  );
};

/* ---------- TAB · FEES ----------
   Fee-revenue breakdown.  Per-method × per-provider × per-brand fee
   collected, PSP cost, net margin, effective % vs the negotiated rate.
   Reads the same shared scope as every other tab. */

/* WHO DECIDED IT, from the row. This hashed the transaction id and picked an
   operator out of a list — so the "fees by operator" report attributed every
   payment to somebody chosen by arithmetic on its id, and the same operator
   got the same payments on every load, which is what made it look like data.

   Both request tables carry `decided_by_id`, embedded as `decidedBy`. A
   request nobody has decided yet has NO operator, and that is returned as null
   rather than hashed into one — an undecided payment belongs to nobody. */
const txOperator = (t) => {
  const d = t && t._raw && t._raw.decidedBy;
  if (!d || d.id == null) return null;
  return { id: Number(d.id), name: d.username, role: "" };
};

const ReportsFees = ({ brand, scoped, currency }) => {
  // Balanced = Approved in this engine (an approved tx has been passed
  // to the PSP and resolved as Balanced or Failed). Treat both as the
  // settled-success terminal state.
  const isBalanced = (t) => t.status === "balanced" || t.status === "completed" || t.status === "approved";
  const isDeposit  = (t) => t.type === "Deposit";
  const isWithdraw = (t) => t.type === "Withdrawal";
  const settled = scoped.filter(isBalanced);

  const depTxs  = settled.filter(isDeposit);
  const wdTxs   = settled.filter(isWithdraw);
  const depVol  = depTxs.reduce((a, t) => a + t.amount, 0);
  const wdVol   = wdTxs.reduce((a, t) => a + t.amount, 0);
  const depFeeR = (() => { let total = 0, missing = 0; depTxs.forEach(t => { const f = txFee(t); if (f == null) missing++; else total += f; }); return { total, missing }; })();
  const wdFeeR  = (() => { let total = 0, missing = 0; wdTxs.forEach(t => { const f = txFee(t); if (f == null) missing++; else total += f; }); return { total, missing }; })();
  const depFee  = depFeeR.total;
  const wdFee   = wdFeeR.total;
  const txVolume   = depVol + wdVol;
  const feesTotal  = depFee + wdFee;
  const effPct     = txVolume > 0 ? (feesTotal / txVolume) * 100 : 0;
  const depEffPct  = depVol > 0 ? (depFee / depVol) * 100 : 0;
  const wdEffPct   = wdVol > 0 ? (wdFee / wdVol) * 100 : 0;

  // Group helper — sums fee / volume per row and splits by direction
  // (deposit / withdrawal). No PSP cost column — the operator can't
  // see PSP cost directly so we don't surface it.
  const groupBy = (keyFn, labelFn) => {
    const map = {};
    for (const t of settled) {
      const k = keyFn(t); if (!k) continue;
      if (!map[k]) map[k] = { key:k, label:labelFn(t), count:0, volume:0, fee:0,
                              dep_count:0, dep_vol:0, dep_fee:0,
                              wd_count:0,  wd_vol:0,  wd_fee:0 };
      const row = map[k];
      /* An unsettled fee is null. Counting it as 0 would put the row's
         effective rate below what it will be, on exactly the newest traffic. */
      const fee  = txFee(t);
      if (fee == null) row.fee_missing = (row.fee_missing || 0) + 1;
      row.count  += 1; row.volume += t.amount; row.fee += (fee || 0);
      if (isDeposit(t))        { row.dep_count += 1; row.dep_vol += t.amount; row.dep_fee += (fee || 0); }
      else if (isWithdraw(t))  { row.wd_count  += 1; row.wd_vol  += t.amount; row.wd_fee  += (fee || 0); }
    }
    return Object.values(map)
      /* The effective rate is NULL when any row in the group is unsettled — a
         percentage computed over a partial numerator reads as a real rate. */
      .map(r => ({ ...r, eff_pct: (r.fee_missing || r.volume <= 0) ? null : +((r.fee / r.volume) * 100).toFixed(2) }))
      .sort((a, b) => b.fee - a.fee);
  };
  const byMethod   = groupBy(t => t.method,   t => t.method_name || t.method);
  const byProvider = groupBy(t => t.provider, t => t.provider);
  const byBrand    = groupBy(t => t.brand,    t => t.brand_name || t.brand);
  const byType     = groupBy(t => t.type,     t => t.type);
  const byPlayer   = groupBy(
    t => t.user_id || t.user || t.player_id,
    t => t.player_name || t.player || t.user_name || (t.user_id || ""));
  const byOperator = (() => {
    const map = {};
    for (const t of settled) {
      const op = txOperator(t);
      if (!op) continue;
      if (!map[op.id]) map[op.id] = { key:op.id, label:op.name, role:op.role, count:0, volume:0, fee:0,
                                       dep_count:0, dep_vol:0, dep_fee:0,
                                       wd_count:0,  wd_vol:0,  wd_fee:0 };
      const row = map[op.id];
      const fee = txFee(t);
      if (fee == null) row.fee_missing = (row.fee_missing || 0) + 1;
      row.count  += 1; row.volume += t.amount; row.fee += (fee || 0);
      if (isDeposit(t))        { row.dep_count += 1; row.dep_vol += t.amount; row.dep_fee += (fee || 0); }
      else if (isWithdraw(t))  { row.wd_count  += 1; row.wd_vol  += t.amount; row.wd_fee  += (fee || 0); }
    }
    return Object.values(map)
      .map(r => ({ ...r, eff_pct: (r.fee_missing || r.volume <= 0) ? null : +((r.fee / r.volume) * 100).toFixed(2) }))
      .sort((a, b) => b.fee - a.fee);
  })();

  // Pretty PSP names.
  const pspNameById = Object.fromEntries(((window.getAllProviders && window.getAllProviders()) || []).map(p => [p.id, p.name]));
  byProvider.forEach(r => { r.label = pspNameById[r.key] || r.label || r.key; });

  const moneyTone = (v) => v >= 0 ? "var(--ok-700)" : "var(--err-700)";

  // KPI card wrapper that supports an inline Tip on the label without
  // touching the base KpiCard signature.
  const FeeKpi = ({ tone, label, value, sub, tip }) => (
    <div style={{position:"relative"}}>
      <KpiCard tone={tone} label={label} value={value} sub={sub}/>
      <span style={{position:"absolute", top:10, right:12}}><Tip>{tip}</Tip></span>
    </div>
  );

  const Table = ({ title, desc, scopeWord, rows, exportName, keyLabel }) => (
    <ReportCard
      title={title}
      desc={desc}
      exportFilename={exportName}
      exportRows={() => rows.map(r => ({
        [keyLabel]: r.label,
        deposit_count: r.dep_count, deposit_volume: Math.round(r.dep_vol), deposit_fee: +r.dep_fee.toFixed(2),
        withdrawal_count: r.wd_count, withdrawal_volume: Math.round(r.wd_vol), withdrawal_fee: +r.wd_fee.toFixed(2),
        total_volume: Math.round(r.volume),
        operator_fee: +r.fee.toFixed(2),
        effective_pct: r.eff_pct,
      }))}
      exportCols={[
        {key:keyLabel,label:keyLabel},
        {key:"deposit_count",label:"deposit_count"},{key:"deposit_volume",label:"deposit_volume"},{key:"deposit_fee",label:"deposit_fee"},
        {key:"withdrawal_count",label:"withdrawal_count"},{key:"withdrawal_volume",label:"withdrawal_volume"},{key:"withdrawal_fee",label:"withdrawal_fee"},
        {key:"total_volume",label:"total_volume"},
        {key:"operator_fee",label:"operator_fee"},
        {key:"effective_pct",label:"effective_pct"},
      ]}>
      <div style={{overflowX:"auto"}}>
      <table className="data-table" style={{width:"100%", minWidth:760}}>
        <thead>
          <tr>
            <th>{scopeWord}</th>
            <th style={{textAlign:"right"}}>
              <span style={{display:"inline-flex", alignItems:"center", justifyContent:"flex-end"}}>
                Deposit fees
                <Tip>What we charged players on every <strong>deposit</strong> in this row. <code>deposit_fee_pct × amount + deposit_fee_fixed</code>. Volume + count are shown below the amount.</Tip>
              </span>
            </th>
            <th style={{textAlign:"right"}}>
              <span style={{display:"inline-flex", alignItems:"center", justifyContent:"flex-end"}}>
                Withdrawal fees
                <Tip>What we charged players on every <strong>withdrawal</strong> in this row. <code>withdraw_fee_pct × amount + withdraw_fee_fixed</code>. Volume + count are shown below the amount.</Tip>
              </span>
            </th>
            <th style={{textAlign:"right"}}>
              <span style={{display:"inline-flex", alignItems:"center", justifyContent:"flex-end"}}>
                Total fees
                <Tip>Total fees the operator collected in this row = Deposit fees + Withdrawal fees. This is the casino's gross fee revenue across both directions.</Tip>
              </span>
            </th>
            <th style={{textAlign:"right"}}>
              <span style={{display:"inline-flex", alignItems:"center", justifyContent:"flex-end"}}>
                Effective %
                <Tip>Total fees ÷ Total volume × 100. The realised average fee rate vs the headline percentage you negotiated with the player. Drift here usually means the fixed-fee component is dominating low-ticket traffic.</Tip>
              </span>
            </th>
          </tr>
        </thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan={5} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No settled transactions in scope.</td></tr>}
          {rows.map(r => (
            <tr key={r.key}>
              <td style={{fontWeight:600}}>
                {r.label}
                {r.role && <div style={{fontSize:10.5, color:"var(--text-tertiary)", fontWeight:500, marginTop:2}}>{r.role}</div>}
              </td>
              <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>
                <div style={{fontWeight:600}}>{fmtMoney(r.dep_fee, currency)}</div>
                <div style={{fontSize:10.5, color:"var(--text-tertiary)"}}>{fmtMoney(r.dep_vol, currency)} · {r.dep_count} tx</div>
              </td>
              <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>
                <div style={{fontWeight:600}}>{fmtMoney(r.wd_fee, currency)}</div>
                <div style={{fontSize:10.5, color:"var(--text-tertiary)"}}>{fmtMoney(r.wd_vol, currency)} · {r.wd_count} tx</div>
              </td>
              <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:700}}>{fmtMoney(r.fee, currency)}</td>
              <td style={{textAlign:"right", fontWeight:600, fontVariantNumeric:"tabular-nums"}}>{fmtPct(r.eff_pct, 2)}</td>
            </tr>
          ))}
        </tbody>
        {rows.length > 0 && (() => {
          const tDepFee = rows.reduce((a, r) => a + r.dep_fee, 0);
          const tDepVol = rows.reduce((a, r) => a + r.dep_vol, 0);
          const tDepN   = rows.reduce((a, r) => a + r.dep_count, 0);
          const tWdFee  = rows.reduce((a, r) => a + r.wd_fee, 0);
          const tWdVol  = rows.reduce((a, r) => a + r.wd_vol, 0);
          const tWdN    = rows.reduce((a, r) => a + r.wd_count, 0);
          const tVol = rows.reduce((a, r) => a + r.volume, 0);
          const tFee = rows.reduce((a, r) => a + r.fee, 0);
          const tEff = tVol > 0 ? (tFee / tVol) * 100 : 0;
          return (
            <tfoot>
              <tr style={{borderTop:"2px solid var(--border-default)", background:"var(--n-25)"}}>
                <td style={{fontWeight:700, padding:"10px 8px"}}>Total · {rows.length} {scopeWord.toLowerCase()}{rows.length===1?"":"s"}</td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>
                  <div>{fmtMoney(tDepFee, currency)}</div>
                  <div style={{fontSize:10.5, color:"var(--text-tertiary)", fontWeight:500}}>{fmtMoney(tDepVol, currency)} · {tDepN} tx</div>
                </td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>
                  <div>{fmtMoney(tWdFee, currency)}</div>
                  <div style={{fontSize:10.5, color:"var(--text-tertiary)", fontWeight:500}}>{fmtMoney(tWdVol, currency)} · {tWdN} tx</div>
                </td>
                <td style={{textAlign:"right", fontWeight:800, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(tFee, currency)}</td>
                <td style={{textAlign:"right", fontWeight:700, fontVariantNumeric:"tabular-nums"}}>{fmtPct(tEff, 2)}</td>
              </tr>
            </tfoot>
          );
        })()}
      </table>
      </div>
    </ReportCard>
  );

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Volume</strong> — the money players moved (deposits in + withdrawals out). Split below into Total deposits and Total withdrawals so you can see each flow.</>,
          <><strong>Operator fees</strong> — what the casino charged the player on top of what the PSP charges. <code>fee = pct × amount + fixed</code>. Split below into Deposit fees and Withdrawal fees.</>,
          <><strong>Effective fee %</strong> = Operator fees ÷ Volume × 100. The realised average fee rate vs the headline % you negotiated.</>,
          <><strong>Settled only</strong> — pending and to-confirm transactions aren't fees we've actually collected yet, so they're excluded. <em>Balanced</em> and <em>Approved</em> are the same outcome here.</>,
        ]}>
        Fee revenue collected by the operator on settled transactions. Below the KPIs you can slice by method · provider · brand · type · <strong>player</strong> · <strong>operator</strong>. The unified filter row at the top still applies (narrow by method, provider, brand, country, etc.).
      </Explainer>

      {/* Row 1 — Volume split: deposits, withdrawals, total volume, effective. */}
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr 1fr", gap:14}}>
        <FeeKpi tone="primary"
          label="Total deposits"
          value={fmtMoney(depVol, currency)}
          sub={`${fmtCount(depTxs.length)} settled deposit${depTxs.length===1?"":"s"}`}
          tip="Sum of every successful Deposit amount in the active scope (Balanced / Approved status — same thing in this engine). Excludes pending and failed transactions."/>
        <FeeKpi tone="warn"
          label="Total withdrawals"
          value={fmtMoney(wdVol, currency)}
          sub={`${fmtCount(wdTxs.length)} settled withdrawal${wdTxs.length===1?"":"s"}`}
          tip="Sum of every successful Withdrawal amount in the active scope. Excludes pending withdrawals (still queued at the PSP) and to-confirm ones (still in the manual review queue)."/>
        <FeeKpi tone="primary"
          label="Total volume"
          value={fmtMoney(txVolume, currency)}
          sub={`${fmtCount(settled.length)} settled transactions`}
          tip="Deposits + Withdrawals. The denominator of the effective fee % and the volume figure exported in every CSV on this tab."/>
        <FeeKpi tone="primary"
          label="Effective fee %"
          value={fmtPct(effPct, 2)}
          sub={`Deposits ${fmtPct(depEffPct, 2)} · Withdrawals ${fmtPct(wdEffPct, 2)}`}
          tip="Operator fees ÷ Total volume × 100. The realised average fee rate across both directions. The sub-row breaks it out per direction so you can see if deposits or withdrawals are dragging the number."/>
      </div>

      {/* Row 2 — Fee revenue split: deposit fees, withdrawal fees, total fees. */}
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:14}}>
        <FeeKpi tone="primary"
          label="Total deposit fees"
          value={fmtMoney(depFee, currency)}
          sub={`${fmtPct(depEffPct, 2)} effective on deposits`}
          tip="Sum of deposit-side operator fees collected = Σ (deposit_fee_pct × amount + deposit_fee_fixed). Drift versus your headline rate usually means the fixed-fee component is dominating low-ticket traffic."/>
        <FeeKpi tone="primary"
          label="Total withdrawal fees"
          value={fmtMoney(wdFee, currency)}
          sub={`${fmtPct(wdEffPct, 2)} effective on withdrawals`}
          tip="Sum of withdrawal-side operator fees collected = Σ (withdraw_fee_pct × amount + withdraw_fee_fixed). Usually higher % than deposits to discourage frequent low-value cash-outs."/>
        <FeeKpi tone="primary"
          label="Total operator fees"
          value={fmtMoney(feesTotal, currency)}
          sub={`${fmtMoney(depFee, currency)} deposits + ${fmtMoney(wdFee, currency)} withdrawals`}
          tip="Deposit fees + Withdrawal fees. The headline operator fee revenue across all directions in this window."/>
      </div>

      <Table title="Fees by payment method"
        desc="Per payment method: deposit fees and withdrawal fees split out, plus the total operator fee and the effective %. Sorted by total fees descending."
        scopeWord="Method" rows={byMethod} exportName="fees-by-method" keyLabel="method"/>

      <Table title="Fees by provider"
        desc="Per PSP that actually processed the transaction after the cascade ran. Use to spot a method × PSP combination whose effective % has drifted out of the negotiated range."
        scopeWord="Provider" rows={byProvider} exportName="fees-by-provider" keyLabel="provider"/>

      <Table title="Fees by brand"
        desc="Per tenant. Used to compare fee revenue between brands when the fee schedule diverges."
        scopeWord="Brand" rows={byBrand} exportName="fees-by-brand" keyLabel="brand"/>

      <Table title="Fees by transaction type"
        desc="Just two rows — Deposit and Withdrawal. The two-row read of the platform-wide fee mix."
        scopeWord="Type" rows={byType} exportName="fees-by-type" keyLabel="type"/>

      <Table title="Fees by player"
        desc="Top players ranked by total fee revenue collected from them. Use to spot the VIPs whose lifetime fee value is highest and the bot rings whose tiny-ticket traffic is generating outsized fixed-fee revenue."
        scopeWord="Player" rows={byPlayer} exportName="fees-by-player" keyLabel="player"/>

      <Table title="Fees by operator"
        desc="Back-office operator productivity in fee revenue. Each row is one of the operators who handled / approved transactions in the active scope (Marco, Elena, Davide, Sara…). The total-fees column is a useful proxy for who's processing the highest-value queues."
        scopeWord="Operator" rows={byOperator} exportName="fees-by-operator" keyLabel="operator"/>
    </div>
  );
};

/* ---------- TAB · LIQUIDITY ----------
   Treasury view of every PSP's wallet.  Drives the daily decision on
   whether to sweep idle capital out into the bank account or top up
   before withdrawals start failing.  Reads the live PSP_PROFILES
   directory (the same source the routing engine and the Provider
   editor read from). */

const ReportsLiquidity = ({ brand }) => {
  /* NO SOURCE, AND THIS TAB IS THE ONE WHERE THAT MATTERS MOST. It is a
     treasury view: it says whether to sweep idle capital out of a PSP wallet or
     top it up before withdrawals start failing. Every figure it needs — the
     wallet balance, the floor, the ceiling, the sweep line — came from
     `window.MOCK_PSP_PROFILES.liquidity`, and this schema has no PSP wallet
     balance anywhere. `payment_providers` carries configuration, not treasury.

     Rendering it from the provider list with zeros would produce a page saying
     every PSP is below its floor, which is an alarm rather than an empty
     report. So the tab says what it needs instead.
     <!-- SUGGESTION: PSP liquidity needs its own table — wallet balance per provider and currency, with a floor, a ceiling and a sweep threshold, refreshed from each PSP's balance API. Nothing in this schema records what sits in a provider's wallet, so the treasury decision this tab exists for cannot be made from it. --> */
  const psps = [];

  // Per-PSP derived state.
  const rows = psps.map(p => {
    const liq = p.liquidity || {};
    const wallet = liq.walletEur || 0;
    const floor = liq.floorEur || 0;
    const ceiling = liq.ceilingEur || 1;
    const sweepPct = liq.sweepPct || 80;
    const sweepLine = (sweepPct / 100) * ceiling;
    const headroom = Math.max(0, ceiling - wallet);
    const aboveSweep = wallet >= sweepLine;
    const belowFloor = wallet < floor;
    const pct = Math.min(100, Math.round((wallet / Math.max(1, ceiling)) * 100));
    const state = belowFloor ? "below_floor"
                : aboveSweep  ? "needs_sweep"
                : "healthy";
    return {
      id: p.id, name: p.name, kind: p.kind,
      settlement: liq.settlement || "—",
      payout: liq.payout || "—",
      minWithdrawal: liq.minWithdrawalEur || 0,
      wallet, floor, ceiling, sweepPct, sweepLine, headroom, pct, state,
    };
  });

  const totalLocked   = rows.reduce((a, r) => a + r.wallet, 0);
  const totalCeiling  = rows.reduce((a, r) => a + r.ceiling, 0);
  const totalFloor    = rows.reduce((a, r) => a + r.floor, 0);
  const totalHeadroom = rows.reduce((a, r) => a + r.headroom, 0);
  const needsSweep    = rows.filter(r => r.state === "needs_sweep").length;
  const belowFloor    = rows.filter(r => r.state === "below_floor").length;
  const healthy       = rows.filter(r => r.state === "healthy").length;
  const aboveFloorPct = rows.length > 0 ? Math.round((rows.length - belowFloor) / rows.length * 100) : 100;

  // Synthesised recent sweep / top-up events — deterministic seed so the
  // demo is stable. Real engine ships with treasury-side wiring.
  /* THE SWEEP HISTORY WAS INVENTED DOWN TO THE OPERATOR'S NAME — a sine-based
     `rand` chose whether each event was a sweep or a top-up, how much moved and
     how long ago, and the operator was picked from four hardcoded names by
     index. A treasury movement log naming a person who did not make it is the
     worst thing on this page.

     There is no source: nothing in this schema records a transfer between the
     platform and a PSP's wallet. Empty, with the reason on screen. */
  const recentEvents = [];

  /* THE COMMENT SAID SO ITSELF: "fake aging by parsing the T+N hint". The
     pending amount was 15% of an invented wallet, and the age was the first
     letter of the provider's id modulo 4 — so whether a PSP was breaching its
     settlement SLA depended on how its id was spelt. `rows` is empty now, so
     this is too; it stays as the shape the panel wants when a settlement
     feed exists. */
  const settlementAging = [];

  const stateTone = (s) => s === "below_floor" ? { bg:"var(--err-50, #fee2e2)", fg:"var(--err-700, #991b1b)", label:"Below floor" }
                         : s === "needs_sweep"  ? { bg:"var(--warn-50, #fef3c7)", fg:"var(--warn-700, #92400e)", label:"Sweep due" }
                         :                         { bg:"var(--ok-50, #d1fae5)", fg:"var(--ok-700, #065f46)", label:"Healthy" };

  const fmtTs = (ts) => {
    const diff = Date.now() - ts;
    if (diff < 3600_000) return `${Math.floor(diff/60_000)}m ago`;
    if (diff < 86400_000) return `${Math.floor(diff/3600_000)}h ago`;
    return `${Math.floor(diff/86400_000)}d ago`;
  };

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr 1fr", gap:14}}>
        <KpiCard tone="primary" label="Locked in PSP wallets"
          value={fmtMoney(totalLocked)}
          sub={`across ${rows.length} active PSP${rows.length===1?"":"s"}`}/>
        <KpiCard tone="primary" label="Free headroom"
          value={fmtMoney(totalHeadroom)}
          sub="Ceiling − wallet · capacity to absorb deposits"/>
        <KpiCard tone={belowFloor === 0 ? "primary" : "warn"} label="PSPs above floor"
          value={`${aboveFloorPct}%`}
          sub={`${healthy} healthy · ${needsSweep} sweep due · ${belowFloor} below floor`}/>
        <KpiCard tone={needsSweep > 0 ? "warn" : "primary"} label="Sweep candidates"
          value={fmtCount(needsSweep)}
          sub={needsSweep > 0 ? "Wallets at or above sweep line" : "Nothing to sweep right now"}/>
      </div>

      <ReportCard
        title="Per-PSP wallet status"
        desc="Live wallet balance against each PSP's floor / sweep line / ceiling. State chip flags wallets that need a top-up or a sweep. Click a row in the live UI to open the provider editor."
        exportFilename="liquidity-by-psp"
        exportRows={() => rows.map(r => ({ psp:r.name, kind:r.kind, wallet:Math.round(r.wallet), floor:Math.round(r.floor), sweep_line:Math.round(r.sweepLine), ceiling:Math.round(r.ceiling), headroom:Math.round(r.headroom), state:r.state, settlement:r.settlement, payout:r.payout, min_withdrawable:Math.round(r.minWithdrawal) }))}
        exportCols={[
          {key:"psp",label:"psp"},{key:"kind",label:"kind"},
          {key:"wallet",label:"wallet"},{key:"floor",label:"floor"},{key:"sweep_line",label:"sweep_line"},
          {key:"ceiling",label:"ceiling"},{key:"headroom",label:"headroom"},
          {key:"state",label:"state"},{key:"settlement",label:"settlement"},
          {key:"payout",label:"payout"},{key:"min_withdrawable",label:"min_withdrawable"},
        ]}>
        <table className="data-table" style={{width:"100%"}}>
          <thead>
            <tr>
              <th>PSP</th>
              <th>State</th>
              <th>Wallet · floor → ceiling</th>
              <th style={{textAlign:"right"}}>Wallet</th>
              <th style={{textAlign:"right"}}>Free headroom</th>
              <th>Settle</th>
              <th>Payout</th>
            </tr>
          </thead>
          <tbody>
            {rows.length === 0 && <tr><td colSpan={7} style={{padding:"22px 14px", color:"var(--text-tertiary)", textAlign:"center"}}>
              <div style={{fontWeight:600, color:"var(--text-secondary)", marginBottom:4}}>PSP liquidity is not recorded in this build</div>
              <div style={{fontSize:12}}>This tab needs a wallet balance per provider and currency, with a floor, a ceiling and a sweep threshold — refreshed from each PSP&rsquo;s balance API. <code>payment_providers</code> holds configuration, not treasury, so there is nothing here to read.</div>
            </td></tr>}
            {rows.map(r => {
              const tone = stateTone(r.state);
              const floorPct = Math.min(100, Math.round((r.floor / Math.max(1, r.ceiling)) * 100));
              const sweepPos = Math.min(100, r.sweepPct);
              return (
                <tr key={r.id}>
                  <td>
                    <div style={{fontWeight:600}}>{r.name}</div>
                    <div style={{fontSize:11, color:"var(--text-tertiary)"}}>{r.kind}</div>
                  </td>
                  <td>
                    <span style={{fontSize:10.5, fontWeight:700, padding:"2px 9px", borderRadius:999, background:tone.bg, color:tone.fg, letterSpacing:".05em", textTransform:"uppercase"}}>
                      {tone.label}
                    </span>
                  </td>
                  <td>
                    <div style={{position:"relative", height:8, borderRadius:999, background:"var(--n-75)", overflow:"visible", minWidth:140}}>
                      <div style={{position:"absolute", inset:0, borderRadius:999, overflow:"hidden"}}>
                        <div style={{height:"100%", width:`${r.pct}%`,
                          background: r.state === "below_floor" ? "var(--err-500, #ef4444)" : r.state === "needs_sweep" ? "var(--warn-500, #f59e0b)" : "var(--ok-500, #10b981)"}}/>
                      </div>
                      <div title={`Floor ${fmtMoney(r.floor)}`} style={{position:"absolute", left:`${floorPct}%`, top:-3, bottom:-3, width:2, background:"var(--err-600, #dc2626)"}}/>
                      <div title={`Sweep at ${r.sweepPct}% · ${fmtMoney(r.sweepLine)}`} style={{position:"absolute", left:`${sweepPos}%`, top:-3, bottom:-3, width:2, background:"var(--warn-500, #f59e0b)"}}/>
                    </div>
                    <div style={{fontSize:10.5, color:"var(--text-tertiary)", marginTop:3}}>
                      Floor {fmtMoney(r.floor)} · Sweep {r.sweepPct}% · Ceiling {fmtMoney(r.ceiling)}
                    </div>
                  </td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmtMoney(r.wallet)}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{fmtMoney(r.headroom)}</td>
                  <td style={{fontSize:11.5, color:"var(--text-secondary)"}}>{r.settlement}</td>
                  <td style={{fontSize:11.5, color:"var(--text-secondary)"}}>{r.payout}</td>
                </tr>
              );
            })}
          </tbody>
          {rows.length > 0 && (
            <tfoot>
              <tr style={{borderTop:"2px solid var(--border-default)", background:"var(--n-25)"}}>
                <td style={{fontWeight:700, padding:"10px 8px"}}>Total · {rows.length} PSP{rows.length===1?"":"s"}</td>
                <td></td>
                <td style={{fontSize:11, color:"var(--text-tertiary)"}}>Floor {fmtMoney(totalFloor)} → Ceiling {fmtMoney(totalCeiling)}</td>
                <td style={{textAlign:"right", fontWeight:800, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(totalLocked)}</td>
                <td style={{textAlign:"right", fontWeight:800, fontVariantNumeric:"tabular-nums"}}>{fmtMoney(totalHeadroom)}</td>
                <td colSpan={2}></td>
              </tr>
            </tfoot>
          )}
        </table>
      </ReportCard>

      <div style={{display:"grid", gridTemplateColumns:"1.2fr 1fr", gap:14}}>
        <ReportCard
          title="Recent sweep & top-up events"
          desc="Treasury actions recorded against PSP wallets. Sweep = funds moved OUT of the PSP into the operator's bank account. Top-up = funds moved IN to the PSP to keep withdrawals alive."
          exportFilename="liquidity-events"
          exportRows={() => recentEvents.map(e => ({ at: new Date(e.ts).toISOString(), kind:e.kind, psp:e.psp, amount:e.amount, operator:e.operator }))}
          exportCols={[{key:"at",label:"at"},{key:"kind",label:"kind"},{key:"psp",label:"psp"},{key:"amount",label:"amount"},{key:"operator",label:"operator"}]}>
          <table className="data-table" style={{width:"100%"}}>
            <thead><tr><th>When</th><th>Kind</th><th>PSP</th><th style={{textAlign:"right"}}>Amount</th><th>Operator</th></tr></thead>
            <tbody>
              {recentEvents.length === 0 && <tr><td colSpan={5} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No events recorded yet.</td></tr>}
              {recentEvents.map(e => (
                <tr key={e.id}>
                  <td style={{fontSize:11, color:"var(--text-tertiary)"}}>{fmtTs(e.ts)}</td>
                  <td>
                    {e.kind === "sweep"
                      ? <span style={{fontSize:10.5, fontWeight:700, padding:"2px 9px", borderRadius:999, background:"var(--p-50)", color:"var(--p-700, #1e3a8a)", letterSpacing:".05em", textTransform:"uppercase"}}>Sweep</span>
                      : <span style={{fontSize:10.5, fontWeight:700, padding:"2px 9px", borderRadius:999, background:"var(--ok-50, #d1fae5)", color:"var(--ok-700, #065f46)", letterSpacing:".05em", textTransform:"uppercase"}}>Top-up</span>}
                  </td>
                  <td style={{fontWeight:600}}>{e.psp}</td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600, color: e.kind === "sweep" ? "var(--p-700, #1e3a8a)" : "var(--ok-700, #065f46)"}}>
                    {e.kind === "sweep" ? "− " : "+ "}{fmtMoney(e.amount)}
                  </td>
                  <td style={{fontSize:11.5, color:"var(--text-secondary)"}}>{e.operator}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </ReportCard>

        <ReportCard
          title="Settlement aging — pending funds vs SLA"
          desc="Money the PSP has not yet settled to our bank, vs the T+0 / T+1 / T+2 SLA each PSP committed to. Breaches surface in red."
          exportFilename="liquidity-settlement-aging"
          exportRows={() => settlementAging.map(s => ({ psp:s.psp, sla_days:s.sla, pending:Math.round(s.pending), aging_days:s.aging, breach: s.breach ? "yes" : "no" }))}
          exportCols={[{key:"psp",label:"psp"},{key:"sla_days",label:"sla_days"},{key:"pending",label:"pending"},{key:"aging_days",label:"aging_days"},{key:"breach",label:"breach"}]}>
          <table className="data-table" style={{width:"100%"}}>
            <thead><tr><th>PSP</th><th>SLA</th><th>Aging</th><th style={{textAlign:"right"}}>Pending</th></tr></thead>
            <tbody>
              {settlementAging.length === 0 && <tr><td colSpan={4} style={{padding:14, color:"var(--text-tertiary)", textAlign:"center"}}>No pending settlements.</td></tr>}
              {settlementAging.map(s => (
                <tr key={s.id}>
                  <td style={{fontWeight:600}}>{s.psp}</td>
                  <td style={{fontSize:11.5, color:"var(--text-secondary)"}}>T+{s.sla}</td>
                  <td>
                    <span style={{fontSize:11.5, fontWeight:700, color: s.breach ? "var(--err-700, #991b1b)" : "var(--text-secondary)"}}>
                      {s.aging}d{s.breach ? " · breached" : ""}
                    </span>
                  </td>
                  <td style={{textAlign:"right", fontVariantNumeric:"tabular-nums", fontWeight:600, color: s.breach ? "var(--err-700, #991b1b)" : "var(--text-primary)"}}>{fmtMoney(s.pending)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </ReportCard>
      </div>
    </div>
  );
};

const Reports = ({ brand, onOpenPlayer }) => {
  window.useLocale && window.useLocale();
  const T = window.T || ((k, fb) => fb || k);
  // Each report tab gets its own URL (e.g. /payments/reports/players) via
  // useUrlTab (src/routes.jsx) — slug reuses the tab id itself since these
  // are already URL-safe (financial, players, geo, conversion, fees,
  // liquidity, ops), with "financial" as the default (no path suffix).
  const REPORT_TAB_ROUTES = [["financial", "financial", ""], ["players", "players", "players"], ["geo", "geo", "geo"], ["conversion", "conversion", "conversion"], ["fees", "fees", "fees"], ["liquidity", "liquidity", "liquidity"], ["ops", "ops", "ops"]];
  const [tab, setTab] = window.useUrlTab("/payments/reports", REPORT_TAB_ROUTES, "financial");
  const [range, setRange] = useState("weekly");
  const [customRange, setCustomRange] = useState(null); // { from, to, days }
  const [customOpen, setCustomOpen] = useState(false);
  // Unified report filter set — applied to the `scoped` slice every
  // report consumes. Brand is a single-select dropdown (matches every
  // other filter on the row); the rest mirror the Transactions filters
  // so the operator already knows the model.
  const [brandFilter, setBrandFilter] = useState("all");
  const [methodFilter, setMethodFilter] = useState("all");
  const [providerFilter, setProviderFilter] = useState("all");
  const [countryFilter, setCountryFilter] = useState("all");
  const [typeFilter, setTypeFilter] = useState("all");
  const [statusFilter, setStatusFilter] = useState("all");
  const [minAmount, setMinAmount] = useState("");
  const [maxAmount, setMaxAmount] = useState("");
  // Promo-code filter — three modes via a dropdown:
  //   "all"  = no filter
  //   "any"  = only transactions that carry any promo code
  //   "none" = only transactions without a promo code
  //   "code" = match exactly the operator-typed promo code in promoCodeInput
  const [promoMode, setPromoMode] = useState("all");
  const [promoCodeInput, setPromoCodeInput] = useState("");

  // Days = number of buckets / window length in days.
  const days = customRange
    ? customRange.days
    : range === "daily" ? 7 : range === "monthly" ? 30 : 14;

  const windowEnd = customRange ? new Date(customRange.to).getTime() : Date.now();
  const windowStart = windowEnd - days * 86400_000;

  const rangeLabel = customRange
    ? `${customRange.from} → ${customRange.to}`
    : range === "daily" ? "Last 7 days"
    : range === "monthly" ? "Last 30 days"
    : "Last 14 days";

  // Adapter — translate the CustomRangePopover's { startMs, endMs }
  // payload into the { from, to, days } shape Reports' window math
  // already uses.
  const applyCustomRange = (r) => {
    if (!r || !r.startMs || !r.endMs) return;
    const from = new Date(r.startMs).toISOString().slice(0, 10);
    const to   = new Date(r.endMs).toISOString().slice(0, 10);
    const d    = Math.min(90, Math.max(1, Math.round((r.endMs - r.startMs) / 86400_000) + 1));
    setCustomRange({ from, to, days: d, label: r.label, startMs: r.startMs, endMs: r.endMs });
  };
  const clearCustom = () => setCustomRange(null);

  // Dynamic scope — top-right brand selector + date range + the unified
  // per-page filter row. Every report pulls from this slice so the
  // controls always agree with what each card shows.
  /* THE WHOLE PAGE HANGS OFF THIS SLICE, so it is the one place the payments
     come from. `txRowFromDeposit` / `txRowFromWithdrawal` (Transactions.jsx)
     are reused rather than re-mapped: two mappers over the same two tables is
     two places for the Fees report and the Transactions list to disagree about
     what a fee is. */
  const cfgDepFeed = useHrsFetch(() => window.sb.list("depositRequests", { limit: 3000 }), []);
  const cfgWdFeed = useHrsFetch(() => window.sb.list("withdrawalRequests", { limit: 3000 }), []);
  const cfgSkinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 200 }), []);
  const cfgMethodFeed = useHrsFetch(() => window.sb.list("paymentMethods", { limit: 300 }), []);
  const cfgProvFeed = useHrsFetch(() => window.sb.list("paymentProviders", { limit: 300 }), []);
  const cfgBusy = cfgDepFeed.loading || cfgWdFeed.loading;
  const cfgErr = cfgDepFeed.error || cfgWdFeed.error;
  const TRANSACTIONS = useMemo(
    () => (cfgDepFeed.data || []).map(txRowFromDeposit)
      .concat((cfgWdFeed.data || []).map(txRowFromWithdrawal))
      .sort((a, b) => b.created_at - a.created_at),
    [cfgDepFeed.data, cfgWdFeed.data]);
  const ALL_BRANDS = useMemo(
    () => (cfgSkinFeed.data || []).map(s => ({ id: Number(s.id), name: s.name, short: String(s.name || "").slice(0, 3).toUpperCase(), currency: s.currency })),
    [cfgSkinFeed.data]);
  const ALL_METHODS = useMemo(
    () => (cfgMethodFeed.data || []).map(m => ({ id: Number(m.id), name: m.name, kind: m.code })),
    [cfgMethodFeed.data]);
  const ALL_PROVIDERS = useMemo(
    () => (cfgProvFeed.data || []).map(p => ({ id: Number(p.id), name: p.name })),
    [cfgProvFeed.data]);
  const brandLocked = brand && !brand.isAll;
  // The brand selected in the per-page filter row. When the top-right
  // selector locks a tenant we ignore the dropdown and force the lock.
  const effectiveBrandId = brandLocked
    ? brand.id
    : (brandFilter === "all" ? null : brandFilter);

  // Distinct country list across the whole transaction pool, sorted.
  /* THE COUNTRY FILTER HAS NO SOURCE ON A PAYMENT. `users.country` exists (028)
     but neither request table carries the country the payment was made from,
     and the two are different facts — a player registered in AR paying from a
     card issued elsewhere. The list is empty rather than derived from the
     player's residence, which would label the payment with the wrong country.
     <!-- SUGGESTION: if payments are to be reported by country, the country belongs on the request (as reported by the PSP), not inferred from users.country — the player's residence is a different fact from where a payment originated. --> */
  const ALL_COUNTRIES = [];

  const minN = parseFloat(minAmount);
  const maxN = parseFloat(maxAmount);
  const promoCodeNeedle = promoCodeInput.trim().toUpperCase();
  // Distinct promo codes actually present in the scope — drives the
  // dropdown suggestions in the filter row.
  const ALL_PROMO_CODES = (() => {
    const set = new Set();
    for (const t of TRANSACTIONS) if (t.promo_code) set.add(t.promo_code);
    return Array.from(set).sort();
  })();
  const scoped = TRANSACTIONS.filter(t => {
    if (effectiveBrandId && t.brand !== effectiveBrandId) return false;
    if (t.created_at < windowStart || t.created_at > windowEnd) return false;
    if (methodFilter   !== "all" && t.method     !== methodFilter)   return false;
    if (providerFilter !== "all" && t.provider   !== providerFilter) return false;
    if (countryFilter  !== "all" && t.country    !== countryFilter)  return false;
    if (typeFilter     !== "all" && t.type       !== typeFilter)     return false;
    if (statusFilter   !== "all") {
      if (statusFilter === "failed") {
        if (!["failed","declined","rejected"].includes(t.status)) return false;
      } else if (t.status !== statusFilter) return false;
    }
    if (!isNaN(minN) && t.amount < minN) return false;
    if (!isNaN(maxN) && t.amount > maxN) return false;
    if (promoMode === "any"  && !t.promo_code) return false;
    if (promoMode === "none" && !!t.promo_code) return false;
    if (promoMode === "code") {
      if (!t.promo_code) return false;
      if (promoCodeNeedle && (t.promo_code || "").toUpperCase() !== promoCodeNeedle) return false;
    }
    return true;
  }).map(t => {
    // A single locked/filtered brand means every row here already shares
    // one real currency — leave amounts as-is (reportCurrency below will
    // label them correctly). Spanning multiple brands mixes ARS/LBP/EUR/
    // BOB/PYG magnitudes, so normalize into EUR before any report sums
    // them — otherwise "Total deposits" etc. would add raw units from
    // different currencies as if they were interchangeable.
    if (effectiveBrandId || t.currency === "EUR") return t;
    return { ...t, amount: window.fxConvert(t.amount, t.currency, "EUR"), fee: window.fxConvert(t.fee || 0, t.currency, "EUR"), currency: "EUR" };
  });
  // The currency every report in this scope should display amounts in —
  // the locked/filtered brand's own currency when one is in effect,
  // otherwise EUR (scoped above is already normalized into EUR for that
  // case, so the two always agree).
  const reportCurrency = brandLocked ? brand.currency
    : effectiveBrandId ? (ALL_BRANDS.find(b => b.id === effectiveBrandId)?.currency || "EUR")
    : "EUR";
  const clearReportFilters = () => {
    setBrandFilter("all");
    setMethodFilter("all"); setProviderFilter("all"); setCountryFilter("all");
    setTypeFilter("all"); setStatusFilter("all");
    setMinAmount(""); setMaxAmount("");
    setPromoMode("all"); setPromoCodeInput("");
  };
  const anyFilter = (brandFilter !== "all" && !brandLocked) || methodFilter !== "all" || providerFilter !== "all"
    || countryFilter !== "all" || typeFilter !== "all" || statusFilter !== "all"
    || minAmount !== "" || maxAmount !== ""
    || promoMode !== "all";

  const TABS = [
    { id:"financial",  label:T("rpt.financial","Financial Report"),icon:"wallet" },
    { id:"players",    label:T("rpt.players","Players"),           icon:"users" },
    { id:"geo",        label:T("rpt.geo","Geo"),                   icon:"globe" },
    { id:"conversion", label:T("rpt.conv","Conversion"),           icon:"check" },
    { id:"fees",       label:T("rpt.fees","Fees"),                 icon:"percent" },
    { id:"liquidity",  label:T("rpt.liquidity","Liquidity"),       icon:"refresh" },
    { id:"ops",        label:T("rpt.ops","Ops & Queue"),           icon:"activity" },
  ];

  const handleExportAll = () => {
    if (!window.PAYBO) return;
    const stamp = new Date().toISOString().slice(0, 10);
    const rows = scoped.map(t => ({
      id:t.id, created_at:new Date(t.created_at).toISOString(),
// EMBED-OK: `t` is a mapped transaction row — `type` is this screen's own key, a label string. `type` is an EMBED on ledger, which this file also reads.
      brand:t.brand_name, method:t.method_name, type:t.type,
      amount:t.amount, currency:t.currency, status:t.status, country:t.country,
    }));
    window.PAYBO.downloadCSV(`paybo-reports-window-${range}-${stamp}.csv`, rows, [
      {key:"id",label:"id"},{key:"created_at",label:"created_at"},
      {key:"brand",label:"brand"},{key:"method",label:"method"},
      {key:"type",label:"type"},{key:"amount",label:"amount"},
      {key:"currency",label:"currency"},{key:"status",label:"status"},
      {key:"country",label:"country"},
    ]);
  };

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title" style={{display:"inline-flex", alignItems:"center"}}>
            {T("page.reports","Reports & analytics")}
            <Tip>
              7 tab areas, each with focused reports: <strong>Financial Report</strong> (deposits − withdrawals = profit + fees collected, broken down by brand / method / provider / type), <strong>Players</strong> (most-active and biggest players, clickable to drill into Player 360), <strong>Geo</strong> (country breakdowns), <strong>Conversion</strong> (approval / decline / retry funnels), <strong>Fees</strong> (fee revenue collected and effective margin per dimension), <strong>Liquidity</strong> (per-PSP wallet health, sweep candidates, settlement aging), <strong>Ops & Queue</strong> (manual review queue + operator productivity). Every report exports to CSV and respects the filter row at the top of the page.
            </Tip>
          </div>
          <div className="page__subtitle">
            {(TABS.find(t => t.id === tab) || {}).label || "Report"} · {brand?.name || "All brands"} · {rangeLabel} · {fmtCount(scoped.length)} transactions in scope
          </div>
        </div>
        <div className="page__actions">
          {/* Report picker — replaces the horizontal tab bar. */}
          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10, boxShadow:"0 1px 2px rgba(15,20,32,.04)"}}>
            <Icon name="chart" size={12} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Report</span>
            <Tip>Pick which report to view. Every report on this page reads the same scope (date range + filter row below), so switching between them keeps your filters intact.</Tip>
            <select value={tab} onChange={e=>setTab(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:13, fontWeight:700, color:"var(--p-700, #1e3a8a)", outline:"none", cursor:"pointer", padding:"4px 6px", minWidth:160}}>
              {TABS.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
            </select>
          </div>
          <div className="segmented" style={{display:"inline-flex", alignItems:"center"}}>
            <button className={range==="daily" && !customRange ? "active" : ""}   onClick={()=>{ setRange("daily"); setCustomRange(null); }}>Daily</button>
            <button className={range==="weekly" && !customRange ? "active" : ""}  onClick={()=>{ setRange("weekly"); setCustomRange(null); }}>Weekly</button>
            <button className={range==="monthly" && !customRange ? "active" : ""} onClick={()=>{ setRange("monthly"); setCustomRange(null); }}>Monthly</button>
            <Tip>
              <strong>Daily</strong> = last 7 days (one bucket per day). <strong>Weekly</strong> = last 14 days. <strong>Monthly</strong> = last 30 days. Use <em>Custom range</em> for an exact window. All reports on this page are recomputed when you change the range.
            </Tip>
          </div>
          <div style={{position:"relative", display:"inline-flex", alignItems:"center"}}>
            <button className="btn btn--secondary btn--sm" onClick={() => setCustomOpen(true)}>
              <Icon name="calendar" size={13}/> {customRange ? rangeLabel : "Custom range"}
            </button>
            <Tip>Pick an exact From / To date range (up to 90 days). Useful for finance close-of-month reports or post-mortem on a specific incident window.</Tip>
            {customRange && (
              <button className="btn btn--ghost btn--icon btn--sm" onClick={clearCustom} title="Clear custom range">
                <Icon name="x" size={11}/>
              </button>
            )}
            {customOpen && (
              <CustomRangePopover
                initial={customRange
                  ? { mode:"range", fromStr: customRange.from, toStr: customRange.to }
                  : { mode:"range" }}
                onApply={(r) => { applyCustomRange(r); setCustomOpen(false); }}
                onCancel={() => setCustomOpen(false)}/>
            )}
          </div>
          <button className="btn btn--secondary btn--sm" onClick={handleExportAll}>
            <Icon name="download" size={13}/> Export window
          </button>
          <Tip>Download a single CSV with every transaction in the active window + brand scope (id, timestamp, brand, method, type, amount, currency, status, country). Individual reports also have their own export buttons.</Tip>
        </div>
      </div>

      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Financial Report</strong> — headline read of the platform: Total Deposits, Total Withdrawals, Profit (= Deposits − Withdrawals), plus Fees collected. Broken down by brand / method / provider / type / status.</>,
          <><strong>Players</strong> — most-active, biggest depositors / withdrawers, top net positions. Player names are clickable and open the player in Player 360.</>,
          <><strong>Geo</strong> — country breakdowns: top countries by volume, average deposit ticket, deposits / withdrawals / net per country.</>,
          <><strong>Conversion</strong> — operator-decision approval rate, top decline reasons, per-method ranking, PSP performance (approval %, p95 latency, decline mix).</>,
          <><strong>Fees</strong> — fee revenue collected on this window plus the effective fee % per dimension. Use to spot a method or provider whose cost has drifted out of the negotiated range.</>,
          <><strong>Liquidity</strong> — treasury view: per-PSP wallet vs floor / ceiling, sweep candidates, recent sweep / top-up events, settlement aging. Drives the daily call on what to sweep out or top up.</>,
          <><strong>Ops &amp; Queue</strong> — manual-review queue snapshot and per-operator productivity.</>,
        ]}>
        Six report areas, every chart and table filtered by the unified row below (date range + brand + method + provider + country + type + status + amount). Each card exports to CSV on its own.
      </Explainer>

      {/* ===== Unified filter row =====
          A single bar drives every report on this page. Every filter is
          a standard dropdown so the row is consistent with Transactions
          and the rest of the back office. When the top-right tenant
          selector locks a brand, the Brand dropdown shows a locked chip
          and is read-only (auto-login). */}
      <div style={{display:"flex", alignItems:"center", gap:8, flexWrap:"wrap", padding:"10px 12px", background:"var(--n-25)", border:"1px solid var(--paybo-border)", borderRadius:10, marginBottom:14}}>
        <Icon name="filter" size={12} style={{color:"var(--text-tertiary)"}}/>
        <span style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", marginRight:2}}>Filter</span>

        {/* Brand — single-select dropdown, matches every other filter on the row. */}
        <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
          <Icon name="flag" size={11} style={{color:"var(--text-tertiary)"}}/>
          <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Brand</span>
          <Tip>Limit every report to one tenant. Pick "All brands" to see the network aggregate. When the top-right brand selector already picks a single brand (auto-login), this dropdown is read-only and shows the locked chip.</Tip>
          {brandLocked ? (
            <span style={{display:"inline-flex", alignItems:"center", gap:5, padding:"2px 8px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", fontSize:11.5, fontWeight:700}}
                  title="Locked by the top-right brand selector">
              <Icon name="lock" size={10}/> {brand.name}
            </span>
          ) : (
            <select value={brandFilter} onChange={e=>setBrandFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All brands</option>
              {ALL_BRANDS.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
            </select>
          )}
        </div>

        {/* Transaction-shape filters (mirror Transactions page) */}
        <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
          <Icon name="credit_card" size={11} style={{color:"var(--text-tertiary)"}}/>
          <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Method</span>
            <Tip>Limit the data to transactions made with one payment method (Visa, Mastercard, Bank wire, Crypto…). Combine with Provider to drill into a single PSP × method combination.</Tip>
            <select value={methodFilter} onChange={e=>setMethodFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All methods</option>
              {ALL_METHODS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
            </select>
          </div>

          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="globe" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Provider</span>
            <Tip>Filter to transactions actually processed by one PSP after the cascade ran (Stripe, Adyen, Trustly…). Different from Method — the same method can route to several providers.</Tip>
            <select value={providerFilter} onChange={e=>setProviderFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All providers</option>
              {ALL_PROVIDERS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </div>

          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="globe" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Country</span>
            <Tip>Resolved from the player's KYC address. Use to build a regional view or to investigate a single geo's behaviour.</Tip>
            <select value={countryFilter} onChange={e=>setCountryFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All countries</option>
              {ALL_COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}
            </select>
          </div>

          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="arrow_down_up" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Type</span>
            <Tip><strong>Deposits</strong> = money in. <strong>Withdrawals</strong> = money out. "All types" keeps both — needed for Profit (Deposits − Withdrawals).</Tip>
            <select value={typeFilter} onChange={e=>setTypeFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All types</option>
              <option value="Deposit">Deposits only</option>
              <option value="Withdrawal">Withdrawals only</option>
            </select>
          </div>

          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="flag" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Status</span>
            <Tip><strong>To-Confirm</strong> = manual review queue. <strong>Approved</strong> = engine or operator approved (now in PSP's hands). <strong>Pending</strong> = sent to the PSP, waiting on a webhook. <strong>Balanced</strong> = terminal success — PSP confirmed the transaction settled. <strong>Failed</strong> covers declined / rejected / errored at any stage. <em>Approval rate</em> and <em>Balanced</em> are two different things: an approval is the operator decision; balanced is the PSP outcome.</Tip>
            <select value={statusFilter} onChange={e=>setStatusFilter(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All statuses</option>
              <option value="balanced">Balanced</option>
              <option value="to_confirm">To-Confirm</option>
              <option value="pending">Pending</option>
              <option value="failed">Failed / declined</option>
            </select>
          </div>

          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 8px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="arrow_up" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Amount</span>
            <Tip>Filter transactions by absolute amount in the brand's currency. Leave a side empty for an open-ended range.</Tip>
            <input value={minAmount} onChange={e=>setMinAmount(e.target.value)} placeholder="min"
              style={{width:64, border:"none", outline:"none", padding:"3px 4px", fontSize:12, fontVariantNumeric:"tabular-nums", background:"transparent"}}/>
            <span style={{color:"var(--text-tertiary)", fontSize:11}}>—</span>
            <input value={maxAmount} onChange={e=>setMaxAmount(e.target.value)} placeholder="max"
              style={{width:64, border:"none", outline:"none", padding:"3px 4px", fontSize:12, fontVariantNumeric:"tabular-nums", background:"transparent"}}/>
          </div>

          {/* Promo-code filter — Any / None / specific code. When "Specific
              code" is picked the operator types the code in the input and
              every report below filters to transactions that used that
              exact code. Useful for "how did WELCOME100 perform?". */}
          <div style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 4px 4px 10px", background:"#fff", border:"1px solid var(--border-default)", borderRadius:10}}>
            <Icon name="percent" size={11} style={{color:"var(--text-tertiary)"}}/>
            <span style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:600}}>Promo code</span>
            <Tip>Filter to transactions that did (or didn't) use a promo / bonus code. Pick <strong>Any code</strong> to see every promo-driven transaction at once, <strong>No code</strong> to exclude bonus traffic, or <strong>Specific code</strong> + type the code (e.g. <code>WELCOME100</code>) to see exactly how that campaign performed. Promo eligibility per method is configured in Payment methods → General → Promotions.</Tip>
            <select value={promoMode} onChange={e=>setPromoMode(e.target.value)}
              style={{border:"none", background:"transparent", fontSize:12, fontWeight:600, color:"var(--text-primary)", outline:"none", cursor:"pointer", padding:"3px 4px"}}>
              <option value="all">All transactions</option>
              <option value="any">Any code applied</option>
              <option value="none">No code applied</option>
              <option value="code">Specific code…</option>
            </select>
            {promoMode === "code" && (
              <>
                <input value={promoCodeInput} onChange={e=>setPromoCodeInput(e.target.value)}
                  list="paybo-promo-codes"
                  placeholder="e.g. WELCOME100"
                  style={{width:130, border:"1px solid var(--border-default)", outline:"none", padding:"3px 6px", fontSize:12, fontWeight:700, letterSpacing:".03em", fontFamily:"var(--font-mono)", borderRadius:6, background:"#fff", textTransform:"uppercase"}}/>
                <datalist id="paybo-promo-codes">
                  {ALL_PROMO_CODES.map(c => <option key={c} value={c}/>)}
                </datalist>
              </>
            )}
          </div>

          <span style={{flex:1}}/>
          <span style={{fontSize:11.5, color:"var(--text-tertiary)"}}>
            <strong style={{color:"var(--text-secondary)"}}>{fmtCount(scoped.length)}</strong> of {fmtCount(TRANSACTIONS.length)} transactions in scope
          </span>
          {anyFilter && (
            <button className="btn btn--ghost btn--sm" onClick={clearReportFilters}>
              <Icon name="x" size={10}/> Clear filters
            </button>
          )}
      </div>

      {tab === "financial"  && <ReportsFinancial brand={brand} scoped={scoped} brandLocked={brandLocked} currency={reportCurrency}/>}
      {tab === "players"    && <ReportsPlayers    brand={brand} scoped={scoped} onOpenPlayer={onOpenPlayer} currency={reportCurrency}/>}
      {tab === "geo"        && <ReportsGeoBrand   brand={brand} scoped={scoped} currency={reportCurrency}/>}
      {tab === "conversion" && <ReportsConversion brand={brand} scoped={scoped} days={days} windowStart={windowStart} windowEnd={windowEnd}/>}
      {tab === "fees"       && <ReportsFees       brand={brand} scoped={scoped} currency={reportCurrency}/>}
      {tab === "liquidity"  && <ReportsLiquidity  brand={brand}/>}
      {tab === "ops"        && <ReportsOps        brand={brand} scoped={scoped} currency={reportCurrency}/>}
    </div>
  );
};

window.Reports = Reports;
