/* page-system.jsx — extracted verbatim from pages.jsx. IIFE-scoped; exports via window. */
(function(){
const { useState, useMemo, useEffect, useRef } = React;
/**
 * @spec GA-090 — Payment gateway status (global)
 * US-080 / US-085 / US-086. Per-franchisee × country row. StatCards sum
 * health counts. Drawer shows event log + mock "Test connection" ping.
 * Spec-02 non-neg #5: never surface secrets/keys/webhooks.
 */
function PaymentStatusGlobal({ role = "GA" }) {
  const [drawerFor, setDrawerFor] = useState(null);
  const [toast, setToast]         = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB5_PAYMENT_STATUS;

  const selected = drawerFor ? SB5_PAYMENT_STATUS.find(r => r.id === drawerFor) : null;

  const onTestConnection = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "payment_gateway.test",
      entity_type: "franchisees", entity_id: selected.franchise_id, country_id: selected.country_id,
      diff: { before: null, after: { provider: selected.provider, ping: "ok", latency_ms: 214 } },
    });
    setToast("Ping successful — logged to audit_logs");
    setTimeout(() => setToast(null), 2800);
  };

  return (
    <div className="page-inner">
      <PageHead title="Integration" description="POS and payment gateway connections per country."/>

      <InlineAlert kind="info">
        Webhook keys, API keys and provider secrets are managed by DX and <strong>never</strong> surface in the admin UI.
        Merchant references shown here are pre-masked identifiers only.
      </InlineAlert>

      <div style={{ font: "700 16px/1.2 var(--font)", color: "var(--forest)", marginBottom: 4 }}>Payment Gateway (read-only)</div>

      <Table
        columns={[
          { label: "Franchisee",    width: 200, sortable: true, sortKey: "franchise_id", render: r => <span style={{ ...T.primary() }}>{_franchiseeLabel(r.franchise_id)}</span> },
          { label: "Country",       width: 140, sortable: true, sortKey: "country_id", render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
          { label: "Provider",      width: 110, sortable: true, sortKey: "provider", render: r => <span style={{ ..._MONO, fontSize: 13, fontWeight: 600, color: "var(--forest)" }}>{r.provider}</span> },
          { label: "Status",        width: 120, sortable: true, sortKey: "status", render: r => <StatusPill status={r.status} kind="payment"/> },
          { label: "Last success",  width: 160, sortable: true, sortKey: "last_success", render: r => _syncCell(r.last_success, "UTC", MOCK_NOW, true) },
          { label: "24h errors",    width: 100, sortable: true, sortKey: "error_count_24h", render: r => <span style={{ ..._MONO, color: r.error_count_24h > 5 ? "var(--berry)" : "var(--forest-deep)" }}>{r.error_count_24h}</span> },
          { label: "Webhook lag",   width: 110, sortable: true, sortKey: "webhook_lag_s", render: r => <span style={{ ..._MONO, ...T_MUTED }}>{r.webhook_lag_s != null ? `${r.webhook_lag_s}s` : "—"}</span> },
        ]}
        rows={rows}
        onRow={r => setDrawerFor(r.id)}
        emptyText="No gateways match these filters."/>

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? `${selected.provider} · ${_countryLabel(selected.country_id)}` : ""}
        subtitle={selected ? _franchiseeLabel(selected.franchise_id) : ""}
        actions={selected && <>
          <Btn variant="secondary" onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="secondary" onClick={onTestConnection}>Test connection</Btn>
        </>}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Provider",        value: selected.provider, mono: true },
            { label: "Status",          value: <StatusPill status={selected.status} kind="payment"/> },
            { label: "Merchant ref",    value: selected.merchant_ref, mono: true },
            { label: "Last success",    value: _syncCell(selected.last_success, "UTC", MOCK_NOW) },
            { label: "24h errors",      value: String(selected.error_count_24h) },
            { label: "Webhook lag",     value: selected.webhook_lag_s != null ? `${selected.webhook_lag_s}s` : "—" },
          ]}/>
          <InlineAlert kind="warn">
            "Test connection" issues a harmless ping to the provider. Writes the result to <strong>audit_logs</strong>.
            Does not expose or modify any keys, webhooks or secrets.
          </InlineAlert>
          <div>
            <div style={{ ...T.primary(1), marginBottom: 10 }}>Event log (latest)</div>
            {(SB5_PAYMENT_EVENTS[selected.id] || []).length === 0 ? (
              <EmptyState icon={null} title="No events" body="No recent events for this gateway."/>
            ) : (
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {(SB5_PAYMENT_EVENTS[selected.id] || []).map((e, i) => (
                  <div key={i} style={{ display: "flex", gap: 10, padding: "10px 12px", background: "var(--mint-soft)", borderRadius: "var(--r)" }}>
                    <span style={{ ..._MONO, ...T_MUTED, minWidth: 150, fontSize: 12 }}>{_ISO_DATE(e.ts)}</span>
                    <span style={{ font: "700 12px/1.2 var(--font-ui)", color: "var(--forest)" }}>{e.kind}</span>
                    <span style={{ ...T_MUTED, flex: 1 }}>{e.msg}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </>}
      </DetailDrawer>

      {/* SB5 rectification · M-4 — POS integrations (read-only) */}
      <div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 8 }}>
        <div style={{ font: "700 16px/1.2 var(--font)", color: "var(--forest)" }}>POS (read-only)</div>
        <InlineAlert kind="info">
          POS sync health is also surfaced on the <strong>outlet detail</strong> page — this table is a global roll-up for triage only.
        </InlineAlert>
        <Table
          columns={[
            { label: "Country",   width: 130, sortable: true, sortKey: "country_id", render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
            { label: "Outlet",    width: 160, sortable: true, sortKey: "outlet_id", render: r => <Mono size="sm">{r.outlet_id}</Mono> },
            { label: "Provider",  width: 120, sortable: true, sortKey: "provider", render: r => <Mono size="sm">{r.provider}</Mono> },
            { label: "Last sync", width: 180, sortable: true, sortKey: "last_sync", render: r => _syncCell(r.last_sync, "UTC", MOCK_NOW, true) },
            { label: "Status",    width: 120, sortable: true, sortKey: "status", render: r => <StatusPill status={r.status} kind="sync"/> },
          ]}
          rows={SB5_POS_STATUS}
          emptyText="No POS integrations registered."/>
      </div>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-091 — Reconciliation view
 * US-082 / US-066. Tabs matched / unmatched / disputed. Drawer shows
 * side-by-side compare (order / unified_transaction / provider record).
 * Escalate-to-dispute writes to audit_logs.
 * Provider-record shape not in spec-03 — logged as OQ-034 (see notes).
 */
function Reconciliation({ role = "GA" }) {
  const [tab, setTab]             = useState("unmatched");
  const [filters, setFilters]     = useState({});
  const [searchQuery, setSearchQuery] = useState("");
  const [drawerFor, setDrawerFor] = useState(null);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast]         = useState(null);
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  if (role !== "GA") return _notAuthorised;

  const filtered = SB5_RECON
    .filter(r => !filters.country  || r.country_id === filters.country)
    .filter(r => !filters.provider || r.provider === filters.provider);

  // M-3 · refunds, filtered on the same country/provider axes
  const refundRows = SB5_REFUNDS
    .filter(r => !filters.country  || r.country_id === filters.country)
    .filter(r => tab !== "refunds" || true); // tab gate handled at render

  const rows = filtered.filter(r => r.status === tab);
  const counts = filtered.reduce((a, r) => { a[r.status] = (a[r.status] || 0) + 1; return a; }, {});
  const refundCount = SB5_REFUNDS
    .filter(r => !filters.country  || r.country_id === filters.country).length;
  const totalVariance = filtered
    .filter(r => r.status !== "matched")
    .reduce((a, r) => a + Math.abs(r.variance || 0), 0);
  const selected = drawerFor ? SB5_RECON.find(r => r.id === drawerFor) : null;

  const escalate = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "reconciliation.dispute.open",
      entity_type: "orders", entity_id: selected.order_id, country_id: selected.country_id,
      diff: { before: { status: selected.status }, after: { status: "disputed", variance: selected.variance, notified: ["franchisee", "dx_reconciliation"] } },
    });
    setConfirmOpen(false);
    setToast(`Escalated to dispute · ${selected.order_id} · logged to audit_logs`);
    setTimeout(() => setToast(null), 2800);
  };

  const fmtMoney = (amt, provider, ccy) => {
    if (amt == null || amt === "") return "—";
    const sign = amt < 0 ? "-" : "";
    return `${sign}${Math.abs(amt).toLocaleString()}`;
  };

  return (
    <div className="page-inner">
      <PageHead title="Reconciliation"
        actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-integrations")}>Open gateway status</Btn>}/>

      <StatPanel>
        <StatCard label="Matched"    value={counts.matched    || 0} sub={`of ${filtered.length} txns`}/>
        <StatCard label="Unmatched"  value={counts.unmatched  || 0} sub={`of ${filtered.length} txns`}/>
        <StatCard label="Disputed"   value={counts.disputed   || 0} sub={`of ${filtered.length} txns`}/>
        <StatCard label="|Variance|" value={totalVariance.toLocaleString()} sub="sum across non-matched"/>
      </StatPanel>

      <ChartCard title="Variance trend" subtitle="Daily |variance| across all countries (7-day)"
        kind="line" data={VARIANCE_TREND_7D} height={140}/>

      <FilterBar
        filters={[
          { key: "country",  label: "Country",  kind: "select", options: [{value:"",label:"All countries"}, ...SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }))] },
          { key: "provider", label: "Provider", kind: "select", options: [{value:"",label:"All providers"}, ...SB5_PAYMENT_PROVIDERS.map(p => ({ value: p, label: p }))] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}
        search={searchQuery}
        onSearch={setSearchQuery}
        searchPlaceholder="Search outlets…"/>

      <Tabs
        tabs={[
          { id: "matched",   label: "Matched",   count: counts.matched   || 0 },
          { id: "unmatched", label: "Unmatched", count: counts.unmatched || 0 },
          { id: "disputed",  label: "Disputed",  count: counts.disputed  || 0 },
          { id: "refunds",   label: "Refunds",   count: refundCount      || 0 }, // M-3
        ]}
        active={tab}
        onChange={t => { setTab(t); setPage(1); }}/>

      {(() => {
        const baseRows = tab === "refunds" ? refundRows : rows;
        const totalPages = Math.max(1, Math.ceil(baseRows.length / pageSize));
        const visibleRows = baseRows.slice((page - 1) * pageSize, page * pageSize);
        return (
          <>
            {tab === "refunds" ? (
              <Table
                columns={[
                  { label: "Franchisee",    width: 180, sortable: true, sortKey: "franchise_id", render: r => <span style={{ ...T.primary() }}>{_franchiseeLabel(r.franchise_id)}</span> },
                  { label: "Order",         width: 150, sortable: true, sortKey: "order_id", render: r => <button onClick={() => _go("ga-order-detail", { id: r.order_id })} style={{ background: "transparent" }}><Mono size="sm">{r.order_id}</Mono></button> },
                  { label: "Amount",        width: 120, sortable: true, sortKey: "amount", render: r => <Mono>{r.amount.toLocaleString()}</Mono> },
                  { label: "Reason",        width: 180, sortable: true, sortKey: "reason_code", render: r => <span style={{ ...T_MUTED, fontSize: 13 }}>{r.reason_code.replace(/_/g, " ")}</span> },
                  { label: "Issued by",     width: 200, sortable: true, sortKey: "issued_by", render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.issued_by_role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.issued_by}</span></div> },
                  { label: "Provider ref",  width: 160, sortable: true, sortKey: "provider_ref", render: r => <Mono size="sm" muted>{r.provider_ref}</Mono> },
                  { label: "Reconciliation",width: 140, sortable: true, sortKey: "recon_status", render: r => <StatusPill status={r.recon_status} kind="reconciliation"/> },
                ]}
                rows={visibleRows}
                emptyText="No refunds match these filters."/>
            ) : (
              <Table
                columns={[
                  { label: "Order",         width: 150, sortable: true, sortKey: "order_id", render: r => <Mono>{r.order_id}</Mono> },
                  { label: "Country",       width: 110, sortable: true, sortKey: "country_id", render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
                  { label: "Provider",      width: 100, sortable: true, sortKey: "provider", render: r => <Mono size="sm">{r.provider}</Mono> },
                  { label: "Order amt",     width: 120, sortable: true, sortKey: "amount_order", render: r => <Mono>{fmtMoney(r.amount_order)}</Mono> },
                  { label: "Provider amt",  width: 120, sortable: true, sortKey: "amount_provider", render: r => <Mono>{fmtMoney(r.amount_provider)}</Mono> },
                  { label: "Variance",      width: 120, sortable: true, sortKey: "variance", render: r => <span style={{ ..._MONO, color: r.variance === 0 ? "var(--forest-deep)" : "var(--berry)", fontWeight: 700 }}>{r.variance === 0 ? "0.00" : fmtMoney(r.variance)}</span> },
                  { label: "Status",        width: 120, sortable: true, sortKey: "status", render: r => <StatusPill status={r.status} kind="reconciliation"/> },
                  { label: "Provider ref",  width: 160, sortable: true, sortKey: "provider_ref", render: r => <Mono size="sm" muted>{r.provider_ref}</Mono> },
                ]}
                rows={visibleRows}
                onRow={r => setDrawerFor(r.id)}
                emptyText={`No ${tab} rows match these filters.`}/>
            )}
            <TableFooter page={page} totalPages={totalPages} onPage={setPage} count={pageSize} onCountChange={c => { setPageSize(c); setPage(1); }}/>
          </>
        );
      })()}

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.order_id : ""}
        subtitle={selected ? `${_countryLabel(selected.country_id)} · ${selected.provider} · ${selected.date}` : ""}
        actions={selected && <>
          <Btn variant="secondary" onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="secondary" onClick={() => _go("ga-order-detail", { id: selected.order_id })}>Open order</Btn>
          {selected.status === "unmatched" && (
            <Btn variant="danger" onClick={() => setConfirmOpen(true)}>Escalate to dispute…</Btn>
          )}
        </>}>
        {selected && <>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T.label }}>orders</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.order_id}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: "var(--forest)" }}>{fmtMoney(selected.amount_order)}</div>
            </div>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T.label }}>unified_transactions</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.txn_ref}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: "var(--forest)" }}>{fmtMoney(selected.amount_order)}</div>
            </div>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T.label }}>provider record</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.provider_ref}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: selected.variance === 0 ? "var(--forest)" : "var(--berry)" }}>{fmtMoney(selected.amount_provider)}</div>
            </div>
          </div>
          <KeyValueGrid items={[
            { label: "Variance",       value: <span style={{ ..._MONO, color: selected.variance === 0 ? "var(--forest-deep)" : "var(--berry)", fontWeight: 700 }}>{selected.variance === 0 ? "0.00" : fmtMoney(selected.variance)}</span> },
            { label: "Status",         value: selected.status },
            { label: "Date",           value: selected.date },
            { label: "Provider",       value: selected.provider, mono: true },
          ]}/>
          <InlineAlert kind="note">
            Provider-record shape is not yet finalised. Figures here are illustrative; DX to confirm.
          </InlineAlert>
        </>}
      </DetailDrawer>

      <ConfirmModal
        open={confirmOpen}
        destructive
        title={selected ? `Escalate ${selected.order_id} to dispute?` : "Escalate to dispute?"}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>This marks the transaction as <strong>disputed</strong> and notifies the franchisee + DX reconciliation.</div>
            <div>Writes an entry to <strong>audit_logs</strong>. You can still resolve from the disputed tab later.</div>
          </div>
        }
        confirmLabel="Escalate to dispute"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={escalate}/>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-100 — Audit log
 * US-034 / US-110 / US-111. Canonical audit view. Filters by actor,
 * role, entity_type, entity_id, country, date range. Row drawer renders
 * the jsonb `diff` as two-column before/after KeyValueGrid.
 * R-9: `diff` shape is jsonb, not schema-fixed — see OQ-011 scope note.
 *
 * M-1 · accepts CM + AM scoped access (spec-02 §Audit).
 * Pass `scope={ countryId, areaId }`; filtering stop-gap by country only
 * because audit rows don't yet carry `area_id` — see OQ-011 / OQ-035.
 * M-6 · diff viewer masks known secret keys and recurses one level.
 */
function AuditLog({ role = "GA", scope }) {
  // Route payload pre-filter (e.g. from GA-050 "View full audit trail" CTA).
  const payload = typeof window !== "undefined" ? window.__sixhands_route_payload : null;
  // Franchisee deep-link (Franchisee Overview "Audit Log →"): filter.franchise_id scopes to the
  // franchise's OWN audit trail (entity_type=franchisees, entity_id=franchise_id) + a removable chip.
  // Limitation: child-entity activity (the franchise's outlets / users / orders) can't be folded in
  // because audit rows carry entity_type/entity_id/country_id but no franchise_id — see OQ-011/OQ-035.
  const ctxFr = payload && payload.filter && payload.filter.franchise_id ? payload.filter.franchise_id : "";
  const preFilter = ctxFr
    ? { entity_type: "franchisees", entity_id: ctxFr }
    : payload
    ? { entity_type: payload.entity_type || "", entity_id: payload.entity_id || "" }
    : {};
  const [filters, setFilters] = useState({ role: "", actor: "", entity_type: "", entity_id: "", country: "", preset: "", ...preFilter });
  const [ctxChip, setCtxChip] = useState(ctxFr ? { label: payload.filter.label } : null);
  const clearCtx = () => { setCtxChip(null); setFilters(f => ({ ...f, entity_type: "", entity_id: "" })); setPage(1); };
  const [searchQuery, setSearchQuery] = useState("");
  const [page, setPage]       = useState(1);
  const [drawerFor, setDrawerFor] = useState(null);
  const PAGE_SIZE = 10;
  // B-1 · Audit Log is GA-only (02-rbac §3: CM/AM/OM = —)
  if (role !== "GA") return _notAuthorised;

  // B-1 · GA-only → full audit set; OQ-011/OQ-035 role-scoping block removed.
  const scopedBase = SB5_AUDIT;

  const presetMatch = (r) => {
    if (filters.preset === "role_escalations") {
      return r.action === "user.role.set" || r.action === "role.escalation.denied";
    }
    return true;
  };

  const rows = scopedBase
    .filter(r => !filters.role        || r.role === filters.role)
    .filter(r => !filters.actor       || r.actor_user_id === filters.actor)
    .filter(r => !filters.entity_type || r.entity_type === filters.entity_type)
    .filter(r => !filters.entity_id   || (r.entity_id || "").toLowerCase().includes(filters.entity_id.toLowerCase()))
    .filter(r => !filters.country     || r.country_id === filters.country)
    .filter(presetMatch);

  const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
  const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
  const selected = drawerFor ? scopedBase.find(r => r.id === drawerFor) : null;
  const entityTypes = Array.from(new Set(scopedBase.map(r => r.entity_type))).sort();
  const actors = SB3_USERS.filter(u => scopedBase.some(a => a.actor_user_id === u.id));

  // M-6 · render a value — mask secrets, recurse one level into nested objects
  const renderValue = (k, v) => {
    if (SECRET_KEYS.has(k)) return <Mono size="sm">••••••••</Mono>;
    if (v === null) return <span style={{ ...T_MUTED, fontStyle: "italic" }}>null</span>;
    if (typeof v === "object") {
      // nested KeyValueGrid, one level deep
      const items = Object.entries(v).map(([kk, vv]) => ({
        label: kk,
        value: SECRET_KEYS.has(kk) ? "••••••••"
             : vv === null          ? "null"
             : typeof vv === "object" ? JSON.stringify(vv)
             : String(vv),
        mono: true,
      }));
      return <div style={{ padding: "6px 0" }}><KeyValueGrid items={items} columns={1}/></div>;
    }
    return <Mono size="sm">{String(v)}</Mono>;
  };

  const buildDiffItems = (diff, side) => {
    if (!diff || !diff[side]) return [];
    return Object.entries(diff[side]).map(([k, v]) => ({
      label: k,
      value: renderValue(k, v),
      mono: false,
    }));
  };

  const scopeLabel =
    role === "GA" ? "Global" :
    role === "CM" ? (scope && scope.countryId ? scope.countryId : "—") :
    role === "AM" ? (scope ? `${scope.countryId || "—"} · ${scope.areaId || "—"}` : "—") :
                    "—";

  return (
    <div className="page-inner">
      <PageHead title="Audit log" description="Immutable record of all admin actions in scope."/>

      <InlineAlert kind="note">
        Audit coverage includes every mutation performed by Global Admins and Country Managers on in-scope entities.
      </InlineAlert>

      <FilterBar
        primaryFilter={{ key: "entity_type", label: "Entity type", options: [
          { value: "", label: "All entities" },
          ...entityTypes.map(t => ({ value: t, label: t })),
        ]}}
        primaryValue={filters.entity_type || ""}
        onPrimaryChange={v => { setFilters(f => ({ ...f, entity_type: v })); setCtxChip(null); setPage(1); }}
        advancedFilters={[
          { key: "role",      label: "Role",      kind: "select", options: [{ value: "", label: "All roles" }, ...["GA","CM","AM","OM"].map(r => ({ value: r, label: r }))] },
          { key: "country",   label: "Country",   kind: "select", options: [{ value: "", label: "All countries" }, ...SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }))] },
          { key: "actor",     label: "Actor",     kind: "select", options: [{ value: "", label: "All actors" }, ...actors.map(u => ({ value: u.id, label: u.full_name }))] },
          { key: "entity_id", label: "Entity id", kind: "text" },
        ]}
        advancedValues={filters}
        onAdvancedChange={(k, v) => { setFilters(f => ({ ...f, [k]: v })); if (k === "entity_id") setCtxChip(null); setPage(1); }}
        onAdvancedReset={() => { setFilters(f => ({ ...f, role: "", country: "", actor: "", entity_id: "" })); setCtxChip(null); setPage(1); }}
        onReset={() => { setFilters({ role: "", actor: "", entity_type: "", entity_id: "", country: "", preset: "" }); setCtxChip(null); setPage(1); }}
        rightActions={
          <ChipToggle size="sm"
            on={filters.preset === "role_escalations"}
            onToggle={() => { setFilters(f => ({ ...f, preset: f.preset === "role_escalations" ? "" : "role_escalations" })); setPage(1); }}>
            Role escalations
          </ChipToggle>
        }
        search={searchQuery}
        onSearch={setSearchQuery}
        searchPlaceholder="Search audit log…"/>
      {ctxChip && (
        <div style={{ marginBottom: 12 }}>
          <span className="chip on">
            Franchisee: {ctxChip.label}
            <button onClick={clearCtx} aria-label="Clear context filter"
              style={{ display: "inline-flex", alignItems: "center", background: "none", border: "none", padding: 0, marginLeft: 2, cursor: "pointer", color: "inherit" }}>
              <LIcon name="X" size={14}/>
            </button>
          </span>
        </div>
      )}

      <Table
        columns={[
          { label: "Timestamp",   width: 170, sortable: true, sortKey: "ts", render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{_ISO_DATE(r.ts)}</span> },
          { label: "Role",        width: 200, sortable: true, sortKey: "role", render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.actor}</span></div> },
          { label: "Action",      width: 220, sortable: true, sortKey: "action", render: r => <span style={{ ..._MONO, color: "var(--forest-deep)" }}>{r.action}</span> },
          { label: "Entity type", width: 160, sortable: true, sortKey: "entity_type", render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{r.entity_type}</span> },
          { label: "Entity id",   width: 160, sortable: true, sortKey: "entity_id", render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{(r.entity_id || "").length > 18 ? (r.entity_id.slice(0, 16) + "…") : r.entity_id}</span> },
          { label: "Country",     width: 110, sortable: true, sortKey: "country_id", render: r => <span style={{ ...T_MUTED }}>{r.country_id ? _countryLabel(r.country_id) : "— (global) —"}</span> },
        ]}
        rows={pageRows}
        onRow={r => setDrawerFor(r.id)}
        emptyText="No audit entries match these filters."/>

      <TableFooter page={page} totalPages={totalPages} onPage={setPage} count={20} onCountChange={() => {}}/>

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.action : ""}
        subtitle={selected ? `${selected.entity_type} · ${selected.entity_id}` : ""}
        width={640}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Timestamp",  value: _ISO_DATE(selected.ts) },
            { label: "Role",       value: <div className="row ai-c" style={{ gap: 6 }}><RoleBadge role={selected.role}/>{selected.actor}</div> },
            { label: "Entity",     value: `${selected.entity_type} · ${selected.entity_id}`, mono: true },
            { label: "Country",    value: selected.country_id ? _countryLabel(selected.country_id) : "— (global) —" },
          ]}/>
          <div>
            <div style={{ ...T.primary(1), marginBottom: 10 }}>Change diff</div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
              <div style={{ ...T_SOFT_CARD, padding: 14 }}>
                <div style={{ ...T.label, color: "var(--berry)" }}>Before</div>
                <div style={{ marginTop: 10 }}>
                  {buildDiffItems(selected.diff, "before").length === 0
                    ? <span style={{ ...T_MUTED }}>— (no prior state — creation) —</span>
                    : <KeyValueGrid items={buildDiffItems(selected.diff, "before")} columns={1}/>}
                </div>
              </div>
              <div style={{ ...T_SOFT_CARD, padding: 14, borderColor: "var(--forest)" }}>
                <div style={{ ...T.label, color: "var(--forest)" }}>After</div>
                <div style={{ marginTop: 10 }}>
                  {buildDiffItems(selected.diff, "after").length === 0
                    ? <span style={{ ...T_MUTED }}>— (deletion) —</span>
                    : <KeyValueGrid items={buildDiffItems(selected.diff, "after")} columns={1}/>}
                </div>
              </div>
            </div>
          </div>
          <InlineAlert kind="note">
            Diffs are stored as jsonb on <strong>audit_logs.diff</strong>. Exact keyset depends on the action; DX-normalised shape to follow.
          </InlineAlert>
        </>}
      </DetailDrawer>
    </div>
  );
}

/**
 * @spec GA-101 — Export log
 * US-112 / US-074 / US-075. Re-run writes to audit_logs. Per spec-02
 * §Export, CM export is OFF in Phase 2 — only GA + HQ exports surface.
 */
function ExportLog({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [searchQuery, setSearchQuery] = useState("");
  const [drawerFor, setDrawerFor] = useState(null);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast] = useState(null);
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  if (role !== "GA") return _notAuthorised;

  const rows = SB5_EXPORTS
    .filter(r => !filters.actor       || r.actor === filters.actor)
    .filter(r => !filters.report_type || r.report_type === filters.report_type);
  const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
  const visibleRows = rows.slice((page - 1) * pageSize, page * pageSize);

  const actors = Array.from(new Set(SB5_EXPORTS.map(r => r.actor)));
  const selected = drawerFor ? SB5_EXPORTS.find(r => r.id === drawerFor) : null;

  const rerun = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "export.rerun",
      entity_type: "export_logs", entity_id: selected.id, country_id: selected.scope,
      diff: { before: null, after: { report_type: selected.report_type, scope: selected.scope, row_count: selected.row_count } },
    });
    setConfirmOpen(false);
    setToast(`Re-run queued · ${selected.report_type} · logged to audit_logs`);
    setTimeout(() => setToast(null), 2800);
  };

  return (
    <div className="page-inner">
      <PageHead title="Export log" description="History of data exports run by admins."/>

      <InlineAlert kind="note">
        Country Manager export is <strong>off</strong> in Phase 2. This view surfaces GA + HQ exports only.
        Backend rejects CM-initiated export jobs; this log therefore contains only GA + HQ actors.
      </InlineAlert>

      <FilterBar
        primaryFilter={{ key: "report_type", label: "Report", options: [
          { value: "", label: "All reports" },
          ...SB5_EXPORT_REPORT_TYPES,
        ]}}
        primaryValue={filters.report_type || ""}
        onPrimaryChange={v => setFilters(f => ({ ...f, report_type: v }))}
        advancedFilters={[
          { key: "actor", label: "Actor", kind: "select", options: [{ value: "", label: "All actors" }, ...actors.map(a => ({ value: a, label: a }))] },
        ]}
        advancedValues={filters}
        onAdvancedChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onAdvancedReset={() => setFilters(f => ({ ...f, actor: "" }))}
        onReset={() => setFilters({})}
        search={searchQuery}
        onSearch={setSearchQuery}
        searchPlaceholder="Search exports…"/>

      {rows.length === 0 ? (
        <EmptyState icon={null}
          title="No exports match these filters"
          body="Try clearing filters, or run a new export from the relevant report surface (Orders / Reconciliation / Users)."/>
      ) : (
        <Table
          columns={[
            { label: "Timestamp",    width: 170, sortable: true, sortKey: "ts", render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{_ISO_DATE(r.ts)}</span> },
            { label: "Role",         width: 180, sortable: true, sortKey: "actor", render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.actor_role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.actor}</span></div> },
            { label: "Report",       width: 200, sortable: true, sortKey: "report_type", render: r => <span style={{ font: "500 13px/1 var(--font-ui)", color: "var(--forest-deep)" }}>{(SB5_EXPORT_REPORT_TYPES.find(t => t.value === r.report_type) || {}).label || r.report_type}</span> },
            { label: "Scope",        width: 130, sortable: true, sortKey: "scope", render: r => <span style={{ ...T_MUTED }}>{r.scope ? _countryLabel(r.scope) : "— (global) —"}</span> },
            { label: "Rows",         width: 100, sortable: true, sortKey: "row_count", render: r => <span style={{ ..._MONO }}>{r.row_count.toLocaleString()}</span> },
            { label: "File hash",    width: 180, sortable: true, sortKey: "file_hash", render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{r.file_hash}</span> },
            { label: "Status",       width: 110, sortable: true, sortKey: "status", render: r => <StatusPill status={r.status} kind="export"/> },
            { label: "Size",         width: 100, sortable: true, sortKey: "file_size_kb", render: r => <span style={{ ..._MONO }}>{_FILE_SIZE(r.file_size_kb)}</span> },
          ]}
          rows={visibleRows}
          onRow={r => setDrawerFor(r.id)}
          emptyText="No exports yet."/>
      )}

      {rows.length > 0 && (
        <TableFooter page={page} totalPages={totalPages} onPage={setPage} count={pageSize} onCountChange={c => { setPageSize(c); setPage(1); }}/>
      )}

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? ((SB5_EXPORT_REPORT_TYPES.find(t => t.value === selected.report_type) || {}).label || selected.report_type) : ""}
        subtitle={selected ? `${selected.id} · ${_ISO_DATE(selected.ts)}` : ""}
        actions={selected && selected.status === "completed" && <>
          <Btn variant="secondary" onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="primary" onClick={() => setConfirmOpen(true)}>Re-run export</Btn>
        </>}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Report type", value: selected.report_type, mono: true },
            { label: "Scope",       value: selected.scope ? _countryLabel(selected.scope) : "— (global) —" },
            { label: "Role",        value: <div className="row ai-c" style={{ gap: 6 }}><RoleBadge role={selected.actor_role}/>{selected.actor}</div> },
            { label: "Row count",   value: selected.row_count.toLocaleString() },
            { label: "File size",   value: _FILE_SIZE(selected.file_size_kb) },
            { label: "File hash",   value: selected.file_hash, mono: true },
            { label: "Status",      value: selected.status },
            { label: "Timestamp",   value: _ISO_DATE(selected.ts) },
          ]}/>
          {selected.status === "failed" && (
            <InlineAlert kind="error">
              This export failed. Re-running queues a fresh job and writes a new <strong>export_logs</strong> row.
            </InlineAlert>
          )}
        </>}
      </DetailDrawer>

      <ConfirmModal
        open={confirmOpen}
        title={selected ? `Re-run "${(SB5_EXPORT_REPORT_TYPES.find(t => t.value === selected.report_type) || {}).label}"?` : "Re-run export?"}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>Queues a new export with the same scope and parameters.</div>
            <div>Writes a new row to <strong>export_logs</strong> and an entry to <strong>audit_logs</strong>. Large exports may take several minutes.</div>
          </div>
        }
        confirmLabel="Queue re-run"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={rerun}/>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

// (T_SOFT_CARD defined at top of file as alias for T.softCard)

// ── Country Manager (T2) ─────────────────────────────────────────────

/**
 * @spec CM-090 — Payment gateway status (read-only, own country)
 */
function CMPaymentStatus({ role = "CM" }) {
  if (role !== "CM") return _notAuthorised;
  const ph = SB2_COUNTRY_BY_ID["PH"];
  return (
    <div className="page-inner">
      <PageHead title="Integration — Philippines"/>
      <InlineAlert kind="info">Payment gateway config is managed by DX. Contact GA to update.</InlineAlert>
      <FormSection title="Gateway status">
        <KeyValueGrid columns={2} items={[
          { label: "Provider",        value: ph?.default_payment_provider || "stripe" },
          { label: "Status",          value: <StatusPill status="connected" kind="payment"/> },
          { label: "Merchant ref",    value: "stripe_ph_••••4291", mono: true },
          { label: "Last webhook",    value: "Today, 09:12 · 214ms" },
          { label: "Last tested",     value: "2026-04-22 · OK" },
          { label: "24h errors",      value: "0" },
        ]}/>
      </FormSection>
    </div>
  );
}

/** @spec OM-080 — Payment status (own outlet) */
function OMPaymentStatus({ role = "OM" }) {
  if (role !== "OM") return _notAuthorised;
  const o = SB3_OUTLET_BY_ID[OM_OUTLET_ID];
  return (
    <div className="page-inner">
      <PageHead title="Integration status"/>
      <InlineAlert kind="success">Payment gateway is operational for this outlet.</InlineAlert>
      <FormSection title="Gateway health">
        <KeyValueGrid columns={2} items={[
          { label: "Provider",          value: "PayMongo" },
          { label: "Status",            value: <StatusPill status="ok" kind="sync"/> },
          { label: "Last transaction",  value: "2026-05-04 09:14" },
          { label: "Success rate (7d)", value: "99.2%" },
          { label: "Avg settlement",    value: "1–2 business days" },
          { label: "Currency",          value: "PHP" },
        ]}/>
      </FormSection>
      <FormSection title="Today's summary">
        <KeyValueGrid columns={2} items={[
          { label: "Transactions",  value: String(o.orders_today) },
          { label: "GMV",           value: o.gmv_today },
          { label: "Refunds",       value: "1" },
          { label: "Failed",        value: "0" },
        ]}/>
      </FormSection>
    </div>
  );
}

/** @spec OM-090 — Reconciliation (own outlet) */
function OMReconciliation({ role = "OM" }) {
  if (role !== "OM") return _notAuthorised;
  const o = SB3_OUTLET_BY_ID[OM_OUTLET_ID];
  const [toast, setToast] = useState(null);
  const rows = [
    { id: "ord_88321", order_amt: "₱ 997",   provider_amt: "₱ 997",   status: "matched",   provider_ref: "ch_3PbXk2" },
    { id: "ord_88319", order_amt: "₱ 414",   provider_amt: "₱ 414",   status: "matched",   provider_ref: "ch_3PbXk1" },
    { id: "ord_88312", order_amt: "₱ 1,025", provider_amt: "₱ 1,000", status: "unmatched", provider_ref: "ch_3PbXk0" },
    { id: "ord_88305", order_amt: "₱ 840",   provider_amt: "₱ 840",   status: "matched",   provider_ref: "ch_3PbXj9" },
  ];
  // ISSUE-029 (2): per-row structured flag reason — preselected with the most common option.
  const FLAG_REASONS = [
    { value: "wrong_amount",     label: "Wrong amount charged" },
    { value: "payment_missing",  label: "Payment not received" },
    { value: "duplicate",        label: "Duplicate transaction" },
    { value: "other",            label: "Other" },
  ];
  const [flagReason, setFlagReason] = useState({});
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
  const visibleRows = rows.slice((page - 1) * pageSize, page * pageSize);
  return (
    <div className="page-inner">
      <PageHead title="Reconciliation"/>
      <InlineAlert kind="info">Compare today's orders against payment provider records. Flag mismatches and your Area Manager will review.</InlineAlert>
      <Table
        columns={[
          // ISSUE-029 (1): Order ID clickable → om-order-detail.
          { label: "Order ID",      width: 140, sortable: true, sortKey: "id", render: r => (
            <button type="button"
              onClick={(e) => { e.stopPropagation(); _go("om-order-detail", { id: r.id }); }}
              style={{ ...T.mono, fontSize: 12, color: "var(--forest)", textDecoration: "underline", background: "none", border: 0, padding: 0, cursor: "pointer" }}>
              {r.id}
            </button>
          ) },
          { label: "Order amount",  width: 130, sortable: true, sortKey: "order_amt", render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{r.order_amt}</span> },
          { label: "Provider",      width: 130, sortable: true, sortKey: "provider_amt", render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{r.provider_amt}</span> },
          { label: "Provider ref",  width: 140, sortable: true, sortKey: "provider_ref", render: r => <span style={{ ...T.mono, fontSize: 11, color: "var(--ink-2)" }}>{r.provider_ref}</span> },
          { label: "Status",        width: 120, sortable: true, sortKey: "status", render: r => <StatusPill status={r.status} kind="reconciliation"/> },
          { label: "Flag reason",   width: 200, render: r => r.status === "unmatched"
              ? (
                <Select value={flagReason[r.id] || "wrong_amount"} onChange={v => setFlagReason(s => ({ ...s, [r.id]: v }))} options={FLAG_REASONS}/>
              )
              : null },
          { label: "",              width: 100, render: r => r.status === "unmatched"
              ? <Btn variant="secondary" size="sm" onClick={() => {
                  const reason = flagReason[r.id] || "wrong_amount";
                  const label = (FLAG_REASONS.find(x => x.value === reason) || {}).label || reason;
                  setToast(`Flagged ${r.id} · "${label}" · AM notified`);
                  setTimeout(() => setToast(null), 3000);
                }}>Flag</Btn>
              : null },
        ]}
        rows={visibleRows}
        emptyText="No transactions today."/>
      <TableFooter page={page} totalPages={totalPages} onPage={setPage} count={pageSize} onCountChange={c => { setPageSize(c); setPage(1); }}/>
      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

// ── AM new screens ───────────────────────────────────────────────────

/** @spec AM-080 — Reconciliation (area) */
function AMReconciliation({ role = "AM" }) {
  if (role !== "AM") return _notAuthorised;
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  const outlets = SB3_OUTLETS.filter(o => o.area_id === AM_AREA_ID);
  // Stable counts (no Math.random on every render) + ISSUE-022 drill-down mock data per outlet.
  const rows = outlets.map(o => ({
    id: o.id, name: o.name,
    matched: 42,
    unmatched: o.id === "SH-PH-001" ? 1 : 0,
    disputed: 0,
    last_recon: "2026-05-04 09:30",
  }));
  const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
  const visibleRows = rows.slice((page - 1) * pageSize, page * pageSize);
  // Mock unmatched transactions keyed by outlet id.
  const UNMATCHED_TXNS = {
    "SH-PH-001": [
      { order_id: "ord_88312", order_amt: "₱ 1,025", provider_amt: "₱ 1,000", variance: "₱ 25", provider_ref: "ch_3PbXk0", note: "Tip line discrepancy — provider settled minus tip." },
    ],
  };
  // ISSUE-022: row click opens DetailDrawer with per-outlet unmatched transactions.
  const [drawerFor, setDrawerFor] = useState(null);
  return (
    <div className="page-inner">
      <PageHead title="Reconciliation — Area"/>
      <InlineAlert kind="info">Monitor reconciliation status across outlets in your area. Outlets flag mismatches; you review and escalate to CM/GA if needed.</InlineAlert>
      <Table
        columns={[
          { label: "Outlet",     sortable: true, sortKey: "name", render: r => <span style={{ ...T.primary() }}>{r.name}</span> },
          { label: "Matched",    width: 90,  sortable: true, sortKey: "matched", render: r => <span style={{ ...T_MUTED }}>{r.matched}</span> },
          { label: "Unmatched",  width: 100, sortable: true, sortKey: "unmatched", render: r => r.unmatched > 0
              ? <span style={{ color: "var(--apricot)", fontWeight: 600 }}>{r.unmatched}</span>
              : <span style={{ ...T_MUTED }}>0</span> },
          { label: "Disputed",   width: 90,  sortable: true, sortKey: "disputed", render: r => <span style={{ ...T_MUTED }}>{r.disputed}</span> },
          { label: "Last recon", width: 160, sortable: true, sortKey: "last_recon", render: r => <span style={{ ...T_MUTED }}>{r.last_recon}</span> },
        ]}
        rows={visibleRows}
        onRow={r => setDrawerFor(r)}
        emptyText="No outlets in this area."/>
      <TableFooter page={page} totalPages={totalPages} onPage={setPage} count={pageSize} onCountChange={c => { setPageSize(c); setPage(1); }}/>

      <DetailDrawer
        open={!!drawerFor}
        onClose={() => setDrawerFor(null)}
        title={drawerFor ? `${drawerFor.name} · unmatched transactions` : ""}
        subtitle={drawerFor ? `Last recon ${drawerFor.last_recon}` : ""}
        actions={<Btn variant="primary" onClick={() => setDrawerFor(null)}>Close</Btn>}
      >
        {drawerFor && (
          <FormSection title="Unmatched transactions">
            {(UNMATCHED_TXNS[drawerFor.id] || []).length === 0
              ? <div style={{ ...T_MUTED }}>No unmatched transactions for this outlet.</div>
              : (
                <Table
                  columns={[
                    { label: "Order ID",     width: 130, sortable: true, sortKey: "order_id", render: r => (
                      <button type="button"
                        onClick={() => { setDrawerFor(null); _go("am-order-detail", { id: r.order_id }); }}
                        style={{ ...T.mono, fontSize: 12, color: "var(--forest)", textDecoration: "underline", background: "none", border: 0, padding: 0, cursor: "pointer" }}>
                        {r.order_id}
                      </button>
                    ) },
                    { label: "Order amt",    width: 110, sortable: true, sortKey: "order_amt", render: r => <span style={T.amount}>{r.order_amt}</span> },
                    { label: "Provider amt", width: 120, sortable: true, sortKey: "provider_amt", render: r => <span style={T.amount}>{r.provider_amt}</span> },
                    { label: "Variance",     width: 100, sortable: true, sortKey: "variance", render: r => <span style={{ ...T.amount, color: "var(--apricot-ink)" }}>{r.variance}</span> },
                    { label: "Provider ref", width: 140, sortable: true, sortKey: "provider_ref", render: r => <span style={{ ...T.mono, fontSize: 11, color: "var(--ink-2)" }}>{r.provider_ref}</span> },
                  ]}
                  rows={UNMATCHED_TXNS[drawerFor.id] || []}
                  emptyText="None."/>
              )}
            {(UNMATCHED_TXNS[drawerFor.id] || []).map(t => (
              <div key={t.order_id} style={{ ...T_MUTED, fontSize: 12 }}>{t.note}</div>
            ))}
          </FormSection>
        )}
      </DetailDrawer>
    </div>
  );
}

Object.assign(window, { PaymentStatusGlobal, Reconciliation, AuditLog, ExportLog, CMPaymentStatus, OMPaymentStatus, OMReconciliation, AMReconciliation });
})();
