/* page-promotions.jsx — Promotions module (03-ui §2). IIFE-scoped; exports via window.
 * Deps: helpers.jsx — promotion.* RBAC keys + _promotionProposer(country_id|object)
 *       mock-data.js — shared CM_PROMOTIONS (new schema; global = country_id null)
 *       StatusPill kinds promo + promo_approval
 * PO 2026-06-05: approval via edit-mode Selects; no bulk/multi-select; read drawer = Close+Edit only.
 */
(function(){
const { useState, useMemo } = React;

// Country opts — matches SB2_COUNTRIES ids.
const COUNTRY_OPTS = [
  { value: "PH", label: "🇵🇭 Philippines" },
  { value: "SG", label: "🇸🇬 Singapore" },
  { value: "ID", label: "🇮🇩 Indonesia" },
  { value: "TH", label: "🇹🇭 Thailand" },
];
const COUNTRY_FLAG = { PH: "🇵🇭", SG: "🇸🇬", ID: "🇮🇩", TH: "🇹🇭" };

// ── GeoScope data builder (canonical — mirrors page-rewards.jsx) ─────────────
const _geoData = () => ({
  countries: (typeof SB2_COUNTRIES !== "undefined" ? SB2_COUNTRIES : (window.SB2_COUNTRIES || []))
    .map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })),
  areas: (typeof SB3_AREAS !== "undefined" ? SB3_AREAS : (window.SB3_AREAS || []))
    .map(a => ({ value: a.id, label: a.name, country_id: a.country_id })),
  outlets: (typeof SB3_OUTLETS !== "undefined" ? SB3_OUTLETS : (window.SB3_OUTLETS || []))
    .map(o => ({ value: o.id, label: o.location, area_id: o.area_id })),
});

// RBAC capability aliases — named per the consumeManifest contract.
// NOTE: helpers.jsx currently only has "promotion.create" = [GA,CM,AM].
// The 5 new keys below (propose/approve/reject/archive/delete) are declared by the
// helpers-prep agent; once landed, _hasRbac will resolve them. Until then these fall
// back to [] (empty array), leaving buttons hidden — safe fail-closed.
const _canCreate  = (role) => _hasRbac(role, "promotion.create");   // GA only per spec
const _canPropose = (role) => _hasRbac(role, "promotion.propose");  // CM only (AM read-only — DX review 2026-06-14)
const _canApprove = (role) => _hasRbac(role, "promotion.approve");  // GA only (single-GA approval)
// NOTE: delete-safety uses the GLOBAL _canDelete(role,{status,isReferenced,rbacOk}) from helpers.jsx (06-status §3) — no local shadow.

// _promotionProposer(country_id) — returns 'CM' when the country's launch_shape includes
// a CM tier, else 'GA' (CM-less shape → GA creates directly). CM-only authoring; AM is
// read-only. Used to gate the New-promotion button.
const _promotionProposerLocal = (countryId) => {
  const countries = (typeof SB2_COUNTRIES !== "undefined" ? SB2_COUNTRIES : window.SB2_COUNTRIES || []);
  const c = countries.find(x => x.country_id === countryId);
  if (!c) return "CM"; // fallback
  return c.launch_shape && c.launch_shape.includes("cm") ? "CM" : "GA";
};
const _getProposer = (countryId) => {
  if (typeof _promotionProposer === "function") return _promotionProposer(countryId);
  return _promotionProposerLocal(countryId);
};

// Single-GA approval chain: CM drafts → pending_ga → GA approves/rejects. No CM-approve tier; AM read-only.

// Promotion data is the shared global CM_PROMOTIONS (mock-data.js) — real user ids,
// covers every approval_state + the global (country_id null) campaign.

// Filter / option lists
const APPROVAL_STATE_OPTS = [
  { value: "",            label: "All statuses"  },
  { value: "draft",       label: "Draft"         },
  { value: "pending_ga",  label: "Pending GA"    },
  { value: "approved",    label: "Approved"      },
  { value: "rejected",    label: "Rejected"      },
];
// promo_type + discount_type are GA-managed Custom Options (OQ-054) — derive from the
// CUSTOM_OPTIONS registry (option_type), not a hardcoded enum. ids are the machine values.
const _coOpts = (type) =>
  (typeof CUSTOM_OPTIONS !== "undefined" ? CUSTOM_OPTIONS : (window.CUSTOM_OPTIONS || []))
    .filter(o => o.option_type === type)
    .map(o => ({ value: o.id, label: o.label }));

const PROMO_TYPE_OPTS  = [{ value: "", label: "All types" }, ..._coOpts("promo_type")];
const DISCOUNT_TYPE_OPTS = _coOpts("discount_type");
const STATUS_OPTS = [
  { value: "",         label: "All statuses" },
  { value: "draft",    label: "Draft"        },
  { value: "active",   label: "Active"       },
  { value: "inactive", label: "Inactive"     },
];

// Helpers for display — labels resolve through the Custom Options registry.
const _promoTypLabel = (v) => {
  const o = _coOpts("promo_type").find(x => x.value === v);
  return o ? o.label : (v || "—");
};
const _discTypLabel = (v) => {
  const o = _coOpts("discount_type").find(x => x.value === v);
  return o ? o.label : (v || "—");
};

const _userName = (id) => {
  if (!id) return "—";
  const u = (typeof SB3_USERS !== "undefined" ? SB3_USERS : []).find(x => x.id === id);
  return u ? u.full_name : id;
};

// Default draft for create
const _emptyDraft = (role, scopeCountry) => ({
  id: null,
  name: "",
  description: "",
  promo_type: "discount_via_pos",
  country_id: (role === "GA") ? null : (scopeCountry || "PH"),
  area_id: null,   // null = country-wide; CM/GA may scope to a single area (DX review 2026-06-14)
  start_date: "",
  end_date: "",
  discount_type: "percent",
  discount_value: "",
  status: "draft",
  approval_state: "draft",
  created_by_user_id: null,
  approved_by: null,
  approved_at: null,
});

// ──────────────────────────────────────────────────────────────────────────────
/**
 * @spec GA-075 · CM-060 · AM-060 · OM-060
 */
function PromotionsList({ role = "GA", scope = {} }) {
  if (!["GA", "CM", "AM", "OM"].includes(role)) return _notAuthorised;

  const isGA = role === "GA";
  const isCM = role === "CM";
  const isAM = role === "AM";
  const isOM = role === "OM";

  // CM/AM scope country (from scope prop, default PH for prototype)
  const scopeCountry = scope && scope.countryId ? scope.countryId : "PH";
  // The role that proposes in the user's scope country
  const proposerRole = _getProposer(scopeCountry);
  const isProposer = (role === proposerRole) && _canPropose(role);

  // Mutable promotion store — drawer create/edit applies drafts here.
  const [promos, setPromos] = useState(() => CM_PROMOTIONS.map(p => ({ ...p })));

  // ── filter + pagination state ──
  const [searchQuery, setSearchQuery]   = useState("");
  const [filters, setFilters]           = useState({});
  const [advFilters, setAdvFilters]     = useState({});
  const [page, setPage]                 = useState(1);
  const [pageSize, setPageSize]         = useState(20);

  // ── drawer state ──
  const [drawerFor, setDrawerFor]       = useState(null); // id of row in read mode
  const [mode, setMode]                 = useState("read"); // read | edit | create
  const [draft, setDraft]               = useState(null);

  // ── modal state ──

  // ── toast ──
  const [toast, setToast] = useState(null);
  const fireToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2400); };

  // ── row scope ──
  const allPromos = useMemo(() => {
    if (isGA)  return promos;
    if (isCM)  return promos.filter(p => p.country_id === scopeCountry || p.country_id === null);
    if (isAM)  return promos.filter(p => p.country_id === scopeCountry);
    /* OM */   return promos.filter(p => (p.country_id === scopeCountry || p.country_id === null) && p.status === "active");
  }, [promos, role, scopeCountry]);

  // ── filtered rows ──
  const rows = useMemo(() => {
    let r = allPromos;
    if (searchQuery) {
      const q = searchQuery.toLowerCase();
      r = r.filter(p => p.name.toLowerCase().includes(q));
    }
    if (isOM) {
      if (filters.status)     r = r.filter(p => p.status === filters.status);
      if (filters.promo_type) r = r.filter(p => p.promo_type === filters.promo_type);
      if (advFilters.date_range) {
        r = r.filter(p => _dtfWindowMatch(advFilters.date_range, p.start_date, p.end_date, p.country_id || scopeCountry));
      }
    } else {
      if (filters.approval_state) r = r.filter(p => p.approval_state === filters.approval_state);
      if (filters.promo_type)     r = r.filter(p => p.promo_type === filters.promo_type);
      if (advFilters.status)      r = r.filter(p => p.status === advFilters.status);
      if (advFilters.date_range) {
        r = r.filter(p => _dtfWindowMatch(advFilters.date_range, p.start_date, p.end_date, p.country_id || scopeCountry));
      }
      // GA: geoscope filter (country-granular — global rows always pass)
      if (isGA && advFilters.geo) {
        const { countries: gc } = advFilters.geo || {};
        if (gc && gc.length > 0) {
          r = r.filter(p => p.country_id === null || gc.includes(p.country_id));
        }
      }
    }
    return r;
  }, [allPromos, searchQuery, filters, advFilters, role, scopeCountry]);

  const totalPages  = Math.max(1, Math.ceil(rows.length / pageSize));
  const visibleRows = rows.slice((page - 1) * pageSize, page * pageSize);

  // ── drawer helpers ──
  const drawerRow  = drawerFor ? promos.find(p => p.id === drawerFor) || null : null;
  const isCreate   = mode === "create";
  const isEdit     = mode === "edit";
  const isForm     = isEdit || isCreate;

  const openRead   = (row) => { setDrawerFor(row.id); setMode("read"); setDraft(null); };
  const closeDrawer = () => { setDrawerFor(null); setMode("read"); setDraft(null); };
  const startEdit  = () => { setDraft({ ...drawerRow }); setMode("edit"); };
  const startCreate = () => {
    setDrawerFor(null);
    setDraft(_emptyDraft(role, scopeCountry));
    setMode("create");
  };
  const setD = (k, v) => setDraft(d => ({ ...d, [k]: v }));

  // ── save ──
  const saveLabel = isCreate
    ? (_canCreate(role) ? "Create promotion" : "Submit for approval")
    : "Save";

  const saveForm = () => {
    if (!draft || !draft.name.trim()) return;
    const rec = { ...draft };

    if (isCreate) {
      // GA direct → approved (+ set approval metadata); proposer (CM) → pending_ga
      if (_canCreate(role)) {
        rec.approval_state = "approved";
        rec.approved_by    = "u_ga_01";
        rec.approved_at    = new Date().toISOString();
      } else if (_canPropose(role)) {
        rec.approval_state = "pending_ga";
      }
      const id = `promo_NEW_${Date.now()}`;
      setPromos(ps => [{ ...rec, id, created_by_user_id: "u_ga_01" }, ...ps]);
      setDrawerFor(id);
      fireToast(_canCreate(role) ? "Promotion created" : "Submitted for approval");
    } else {
      // Edit mode — respect draft.approval_state from the Status & approval Select (GA/CM tier)
      // Tier actors advancing to approved must stamp approved_by + approved_at.
      if (_canApprove(role) && rec.approval_state === "approved" && !(rec.approved_by)) {
        rec.approved_by = "u_ga_01";
        rec.approved_at = new Date().toISOString();
      }
      // Proposer (CM) saving a draft/rejected row re-submits into the chain
      const isProposerResave = _canPropose(role) && !_canApprove(role) &&
        (rec.approval_state === "draft" || rec.approval_state === "rejected");
      if (isProposerResave) {
        rec.approval_state = "pending_ga";
      }
      setPromos(ps => ps.map(p => p.id === rec.id ? { ...p, ...rec } : p));
      fireToast("Promotion saved");
    }
    setMode("read");
    setDraft(null);
  };

  // ── single-row delete (GA-only, draft rows) ──

  // ── page descriptions ──
  const titleDesc = isGA  ? "Discount & purchase-with-purchase offers."
                  : isCM  ? "Your country's offers."
                  : isAM  ? "Offers live in your area."
                  : /* OM */ "Offers live at your outlet.";

  // ── filter config ──
  const primaryFilter = isOM
    ? [
        { key: "status",     label: "Status",    options: STATUS_OPTS.filter(o => o.value !== "draft") },
        { key: "promo_type", label: "Type",       options: PROMO_TYPE_OPTS },
      ]
    : [
        { key: "approval_state", label: "Approval", options: APPROVAL_STATE_OPTS },
        { key: "promo_type",     label: "Type",      options: PROMO_TYPE_OPTS },
      ];

  const geo = _geoData();
  const advancedFilters = [
    ...(isOM ? [] : [{ key: "status", kind: "select", label: "Status", options: STATUS_OPTS }]),
    { key: "date_range", kind: "datetime", label: "Active during" },
    ...(isGA ? [{
      key: "geo", kind: "geoscope", label: "Network scope", role,
      countries: geo.countries, areas: geo.areas, outlets: geo.outlets,
    }] : []),
  ];

  // ── columns ──
  const colChevron = { label: "", width: 36, render: () => <span style={{ color: "var(--forest)" }}>{Ic.chevR(16)}</span> };

  const colsBase = isOM
    ? [
        { label: "Promotion",  sortable: true, sortKey: "name",       render: r => <span style={{ ...T.primary() }}>{r.name}</span> },
        { label: "Type",       width: 200, sortable: true, sortKey: "promo_type",  render: r => <span style={T_MUTED}>{_promoTypLabel(r.promo_type)}</span> },
        { label: "Starts",     width: 160, sortable: true, sortKey: "start_date",  render: r => <span style={T_MUTED}>{_fmtDateTime(r.start_date)}</span> },
        { label: "Ends",       width: 160, sortable: true, sortKey: "end_date",    render: r => <span style={T_MUTED}>{_fmtDateTime(r.end_date)}</span> },
        { label: "Status",     width: 110, sortable: true, sortKey: "status",      render: r => <StatusPill status={r.status} kind="promo"/> },
        colChevron,
      ]
    : [
        { label: "Promotion",  sortable: true, sortKey: "name",         render: r => <span style={{ ...T.primary() }}>{r.name}</span> },
        { label: "Description",width: 240, sortable: true, sortKey: "description", render: r => <span style={T_MUTED}>{r.description ? (r.description.length > 60 ? r.description.slice(0, 60) + "…" : r.description) : "—"}</span> },
        { label: "Type",       width: 170, sortable: true, sortKey: "promo_type",  render: r => <span style={T_MUTED}>{_promoTypLabel(r.promo_type)}</span> },
        ...(isGA ? [{ label: "Country",  width: 160, sortable: true, sortKey: "country_id", render: r => <span style={T_MUTED}>{r.country_id ? `${COUNTRY_FLAG[r.country_id] || ""} ${r.country_id}` : "🌐 Global"}</span> }] : []),
        { label: "Starts",     width: 160, sortable: true, sortKey: "start_date",   render: r => <span style={T_MUTED}>{_fmtDateTime(r.start_date)}</span> },
        { label: "Ends",       width: 160, sortable: true, sortKey: "end_date",     render: r => <span style={T_MUTED}>{_fmtDateTime(r.end_date)}</span> },
        { label: "Approval",   width: 140, sortable: true, sortKey: "approval_state", render: r => <StatusPill status={r.approval_state} kind="promo_approval"/> },
        { label: "Status",     width: 110, sortable: true, sortKey: "status",       render: r => <StatusPill status={r.status} kind="promo"/> },
        colChevron,
      ];

  // ── drawer form (create + edit) ──
  // Status & approval section — GA only (single-GA approval; no CM tier).
  const showApprovalSection = isForm && !isCreate && draft && isGA;

  // Options for the approval_state Select inside Status & approval section (GA states only)
  const approvalStateEditOpts = [
    { value: "pending_ga",  label: "Pending GA" },
    { value: "approved",    label: "Approved"   },
    { value: "rejected",    label: "Rejected"   },
  ];

  // Remarks sub-block — keeps _useRemarks (a hook) at a stable top level, not inside the drawer JSX.
  // RemarksSection renders its own "Remarks" header — render direct, never inside a titled FormSection.
  function PromotionRemarks({ promoId, seed }) {
    const [remarks, addRemark, editRemark, deleteRemark] = _useRemarks("promotion", promoId, seed || []);
    return <RemarksSection entityType="promotion" entityId={promoId} remarks={remarks} onAdd={addRemark} currentUser="you" onEdit={editRemark} onDelete={deleteRemark} role={role}/>;
  }

  // Area scope (DX review 2026-06-14): a promotion may target one area or stay country-wide.
  // Options derive from the selected country; a global promo (no country) can only be country-wide.
  const formCountry  = draft ? (draft.country_id || (isGA ? null : scopeCountry)) : null;
  const areaOptions  = formCountry
    ? SB3_AREAS.filter(a => a.country_id === formCountry).map(a => ({ value: a.id, label: a.name }))
    : [];

  const formContent = draft && (
    <>
      <FormSection title="General">
        <Field label="Name" required>
          <Input value={draft.name} onChange={v => setD("name", v)} placeholder="e.g. Weekend Loyalty Boost"/>
        </Field>
        <Field label="Description" hint="Promo intent — seeds the ≤10-word subtitle; reviewed for brand alignment.">
          <Textarea value={draft.description || ""} onChange={v => setD("description", v)} rows={2} placeholder="e.g. 10% off bowls, weekday lunch, BGC outlets"/>
        </Field>
        <Field label="Country">
          {isGA ? (
            <Select
              value={draft.country_id == null ? "ALL" : draft.country_id}
              onChange={v => setDraft(d => ({ ...d, country_id: v === "ALL" ? null : v, area_id: null }))}
              options={[{ value: "ALL", label: "🌐 All countries (global)" }, ...COUNTRY_OPTS]}
            />
          ) : (
            <Input value={scopeCountry} disabled/>
          )}
        </Field>
        <Field label="Area scope" hint="Limit to one area, or leave country-wide.">
          <Select
            value={draft.area_id == null ? "ALL" : draft.area_id}
            onChange={v => setD("area_id", v === "ALL" ? null : v)}
            options={[{ value: "ALL", label: "All areas (country-wide)" }, ...areaOptions]}
          />
        </Field>
      </FormSection>

      <FormSection title="Discount">
        <Field label="Promotion type">
          <Select
            value={draft.promo_type}
            onChange={v => setD("promo_type", v)}
            options={PROMO_TYPE_OPTS.filter(o => o.value !== "")}
          />
        </Field>
        {draft.promo_type === "discount_via_pos" && (
          <>
            <Field label="Discount type">
              <Select value={draft.discount_type || "percent"} onChange={v => setD("discount_type", v)} options={DISCOUNT_TYPE_OPTS}/>
            </Field>
            <Field label="Discount value" hint={draft.discount_type === "flat" ? "Amount in local currency" : "Percentage (e.g. 15)"}>
              <Input value={draft.discount_value || ""} onChange={v => setD("discount_value", v)} placeholder={draft.discount_type === "flat" ? "e.g. 50" : "e.g. 15"}/>
            </Field>
          </>
        )}
      </FormSection>

      <FormSection title="Window">
        <Field label="Start">
          <Input type="datetime-local" value={draft.start_date || ""} onChange={v => setD("start_date", v)}/>
        </Field>
        <Field label="End">
          <Input type="datetime-local" value={draft.end_date || ""} onChange={v => setD("end_date", v)}/>
        </Field>
      </FormSection>

      <FormSection title="Status">
        <Field label="Lifecycle status">
          <Select
            value={draft.status || "draft"}
            onChange={v => setD("status", v)}
            options={[
              { value: "draft",    label: "Draft"    },
              { value: "active",   label: "Active"   },
              { value: "inactive", label: "Inactive" },
            ]}
          />
        </Field>
        {showApprovalSection && (
          <Field label="Approval">
            <Select
              value={draft.approval_state || "pending_ga"}
              onChange={v => setD("approval_state", v)}
              options={approvalStateEditOpts}
            />
          </Field>
        )}
      </FormSection>

      {/* Remarks — shown in edit (existing record), hidden in create (no record yet); convention 6 */}
      {isEdit && <PromotionRemarks promoId={draft.id} seed={draft.promotion_remarks}/>}
    </>
  );

  // ── approval-card actions (read mode) ──
  const _patchPromo = (id, patch) => setPromos(ps => ps.map(p => p.id === id ? { ...p, ...patch } : p));
  const approvePromo = (id) => { _patchPromo(id, { approval_state: "approved", approved_by: "u_ga_01", approved_at: new Date().toISOString() }); fireToast("Promotion approved"); };
  const rejectPromo  = (id, reason) => { _patchPromo(id, { approval_state: "rejected", rejection_reason: reason }); fireToast("Promotion rejected"); };
  const withdrawPromo = (id) => { _patchPromo(id, { approval_state: "draft" }); fireToast("Promotion withdrawn to draft"); };

  // ── approval-card one-liner message per state ──
  const _approvalMessage = (r) =>
    r.approval_state === "pending_ga" ? "Awaiting Global Admin approval."
    : r.approval_state === "approved" ? `Approved by ${_userName(r.approved_by)}${r.approved_at ? " · " + _fmtDateTime(r.approved_at) : ""}`
    : r.approval_state === "rejected" ? "Rejected: " + (r.rejection_reason || "—")
    : "Draft — not yet submitted.";

  // ── read mode drawer content ──
  const readContent = !isForm && drawerRow && (() => {
    const r = drawerRow;
    const canApprove  = isGA && r.approval_state === "pending_ga";
    const canWithdraw = isCM && _canPropose(role) && r.approval_state === "pending_ga";
    return (
      <>
        <ApprovalCard
          status={r.approval_state}
          kind="promo_approval"
          message={_approvalMessage(r)}
          canApprove={canApprove}
          canWithdraw={canWithdraw}
          onApprove={() => approvePromo(r.id)}
          onReject={reason => rejectPromo(r.id, reason)}
          onWithdraw={() => withdrawPromo(r.id)}
        />

        <FormSection title="General">
          <KeyValueGrid items={[
            { label: "Name",         value: r.name },
            { label: "Description",  value: r.description || "—" },
            { label: "Country",      value: r.country_id ? `${COUNTRY_FLAG[r.country_id] || ""} ${r.country_id}` : "🌐 Global" },
            { label: "Area scope",   value: r.area_id ? _areaLabel(r.area_id) : "All areas (country-wide)" },
            { label: "Status",       value: <StatusPill status={r.status} kind="promo"/> },
          ]}/>
        </FormSection>

        <FormSection title="Approval">
          <KeyValueGrid items={[
            { label: "Approval state", value: <StatusPill status={r.approval_state} kind="promo_approval"/> },
            { label: "Proposed by",    value: _userName(r.created_by_user_id) },
            { label: "Approved by",    value: _userName(r.approved_by) },
            { label: "Approved at",    value: r.approved_at ? _fmtDateTime(r.approved_at) : "—" },
          ]}/>
        </FormSection>

        <FormSection title="Discount">
          <KeyValueGrid items={[
            { label: "Promotion type", value: _promoTypLabel(r.promo_type) },
            { label: "Discount type",  value: r.discount_type ? _discTypLabel(r.discount_type) : "—" },
            { label: "Discount value", value: r.discount_value || "—" },
          ]}/>
        </FormSection>

        <FormSection title="Window">
          <KeyValueGrid items={[
            { label: "Start",  value: _fmtDateTime(r.start_date) },
            { label: "End",    value: _fmtDateTime(r.end_date) },
          ]}/>
        </FormSection>

        {/* Remarks (Ops-module convention 6; own-author add/edit/delete) */}
        <PromotionRemarks promoId={r.id} seed={r.promotion_remarks}/>

        {!isOM && (
          <DeleteSection
            entityLabel="promotion"
            soft={r.status !== "draft"}
            note={r.status === "draft"
              ? "Permanently delete this draft promotion."
              : "Decommission (soft-delete) this promotion. Historical usage retained. GA-only."}
            gate={_canDelete(role, { status: r.status, isReferenced: false, rbacOk: !isOM })}
            confirmBody={<div>{r.status === "draft" ? "Permanently delete" : "Decommission"} <strong>{r.name}</strong>? Writes to <strong>audit_logs</strong>.</div>}
            onDelete={() => { setPromos(ps => ps.filter(p => p.id !== r.id)); closeDrawer(); }}
          />
        )}
      </>
    );
  })();

  // ── drawer actions ──
  const canEditDrawer = !isOM && drawerRow && (
    isGA ||
    (isCM && drawerRow.country_id === scopeCountry &&
     (drawerRow.approval_state === "draft" || drawerRow.approval_state === "rejected"))
  );
  // GA can delete a draft promotion from the read drawer

  const drawerActions = isForm ? (
    <>
      <Btn variant="secondary" onClick={() => { if (isCreate) closeDrawer(); else { setMode("read"); setDraft(null); } }}>Cancel</Btn>
      <Btn variant="primary" onClick={saveForm} disabled={!draft || !draft.name.trim()}>{saveLabel}</Btn>
    </>
  ) : (
    <>
      <Btn variant="secondary" onClick={closeDrawer}>Close</Btn>
      {canEditDrawer && <Btn variant="primary" onClick={startEdit}>Edit</Btn>}
    </>
  );

  const drawerTitle = isCreate ? "New promotion" : (drawerRow ? drawerRow.name : "");
  const drawerSubtitle = isCreate ? "" : (drawerRow ? _promoTypLabel(drawerRow.promo_type) : "");

  // ── add-new button ──
  const showCreate = isGA && _canCreate(role);
  // CM-only authoring (DX review 2026-06-14, pending Tricia confirm — OQ): CM drafts → pending_ga →
  // GA approves. AM is read-only (no New-promotion button). A CM draft always lands `pending_ga`.
  const showPropose = isCM && _canPropose(role);
  const headActions = (
    <>
      <ExportButton role={role} variant="secondary" onClick={() => fireToast("Exporting promotions…")}/>
      {showCreate  && <Btn variant="primary" onClick={startCreate}>New promotion</Btn>}
      {showPropose && <Btn variant="primary" onClick={startCreate}>New promotion</Btn>}
    </>
  );

  return (
    <div className="page-inner">
      <PageHead
        title="Promotions"
        description={titleDesc}
        actions={headActions}
      />

      <FilterBar
        primaryFilter={primaryFilter}
        primaryValue={filters}
        onPrimaryChange={next => { setFilters(f => ({ ...f, ...next })); setPage(1); }}
        advancedFilters={advancedFilters}
        advancedValues={advFilters}
        onAdvancedChange={(k, v) => { setAdvFilters(f => ({ ...f, [k]: v })); setPage(1); }}
        onAdvancedReset={() => { setAdvFilters({}); setPage(1); }}
        onReset={() => { setFilters({}); setAdvFilters({}); setSearchQuery(""); setPage(1); }}
        search={searchQuery}
        onSearch={q => { setSearchQuery(q); setPage(1); }}
        searchPlaceholder="Search promotions…"
      />

      <Table
        columns={colsBase}
        rows={visibleRows}
        onRow={openRead}
        emptyText="No promotions match these filters."
      />

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

      {/* ── Detail Drawer (read | edit | create) ── */}
      <DetailDrawer
        open={isCreate || !!drawerFor}
        onClose={closeDrawer}
        title={drawerTitle}
        subtitle={drawerSubtitle}
        actions={drawerActions}
        width={700}
      >
        {isForm && formContent}
        {!isForm && drawerFor && readContent}
      </DetailDrawer>

      {toast && <Toast message={toast}/>}
    </div>
  );
}

Object.assign(window, { PromotionsList });
})();
