// Represents: Admin/CommissionCashbackPaymentController — see docs/ISYSTEM_REFERENCE.md §Batch 4 "Acca bonus payments + Commission cashback"
/* ====================================================================
   COMMISSION CASHBACK — PAYMENTS (tab).  GAP FILL: no prototype page
   existed for this screen; built from the reference only.  REAL MONEY.
   ====================================================================
   Real screen: `GET /commission_cashback/payment`
   (`admin.commission_cashback.payment.index`, routes/admin.php:235, nested
   `payment.` group L234 inside the `commission_cashback.` group L232) →
   Admin\CommissionCashbackPaymentController::index (L50; the column map is
   built in __construct L20). Siblings:
     admin.commission_cashback.payment.rows   GET  /commission_cashback/payment/rows   (L236, ::rows L81)
     admin.commission_cashback.payment.info   GET  /commission_cashback/payment/info   (L237, ::info L335)
     admin.commission_cashback.payment.search GET  /commission_cashback/payment/search (L238 — DEAD, see below)
     admin.commission_cashback.payment.delete DELETE /commission_cashback/payment/{id} (L239, ::delete L264)
     admin.commission_cashback.payment.edit   GET  /commission_cashback/payment/{id}/edit   (L240, ::edit L242)
     admin.commission_cashback.payment.update POST /commission_cashback/payment/{id}/update (L241, ::update L280)
     admin.commission_cashback.payment.pay    POST /commission_cashback/payment/{id}/pay    (L242, ::pay L319)
     admin.commission_cashback.payment.show   GET  /commission_cashback/payment/{id}        (L243, ::show L253)
   Views: admin/generics/index.blade.php (headers passed separately via
   $headers, index L54-68) + admin/generics/filters/commissions_cashback_payment.blade.php
   + admin/generics/models/commission_cashback_payment.blade.php
   + public/js/pages/generic/commission_cashback_payment.js.
   No FormRequest — update() validates inline (L286-289): calculated_amount
   `required|numeric|gt:0`, edit_reason `required|string`, then stamps
   `edited_by` (L308).

   THE SIBLING CONFIG SCREEN IS NOT BUILT HERE. `GET /commission_cashback`
   (Admin\CommissionCashbackController — periodicity / days / percentage /
   balance type / auto-pay) is its own page; this file only renders the
   "Settings" half of the shared tab bar as a link to it. The tab bar is real:
   filters/commission_cashback.blade.php:13-18 and
   filters/commissions_cashback_payment.blade.php:13-18 both render it.

   URL-ONLY — SAID OUT LOUD ON THE PAGE. Neither this tab nor its parent has a
   reachable sidebar entry. The "Commission cashback" item (sidebar.blade.php:
   905-912, inside `@if( isadmin() )` L905) sits inside a commented-out short-tag
   block opened at L890 and closed at L913, so it never renders; the payments tab has
   no sidebar entry of its own at all. Both are reachable only by typing the
   URL. That is surfaced in the Explainer rather than quietly "fixed" by
   inventing a nav item.

   WHAT MOVES THE MONEY. `CommissionCashbackPayment::pay()` (Model L50-89) —
   not the platform-wide processTransfer engine that the Deposit/Transfer
   screen and the *commission* payments screen funnel through
   (docs/ISYSTEM_REFERENCE.md §Batch 5 "Deposit"; §Batch 3 commission payments
   → CommissionsController::payCommissions → TransferController::processTransfer).
   This model is its own, smaller flow:
     · DB transaction, row `lockForUpdate`, `paid` re-checked inside the lock →
       genuinely idempotent, so a double submit cannot pay twice (processTransfer
       by contrast has no idempotency key at all);
     · credits the player's `balance` or `balance_withdrawable` according to the
       PARENT cashback's `balance_type`;
     · writes `TransactionHistoryMongo` with `transaction_type = 121`
       (TransactionHistoryType::COMMISSION_CASHBACK) and `res_type = 6`
       (Model L106-119);
     · stamps `paid = 1`, `paid_by`, `paid_at`.
   // UNCLEAR: the reference names no payer/funding account for this credit —
   //   unlike processTransfer, which debits the recipient's skin-admin ancestor
   //   and writes two mirrored `transactions` rows. Only the player-side credit
   //   and the single history row are documented, so this screen shows only a
   //   credit side and never invents a "From" account.
   // UNCLEAR: the payment row carries no currency column (schema: cc_id,
   //   user_id, paid, paid_by, paid_at, from, to, base_amount,
   //   calculated_amount, min/max_base_amount, cashback_percentage, edited_by,
   //   edit_reason). The edit modal is labelled "amount + player currency", so
   //   currency here is resolved from the player's skin.

   DIVERGENCES implemented as evident intent (known-bug policy):
   1. MONEY-MOVING BULK PAY VIA A GET SIDE EFFECT. On the real screen "Pay all"
      and "Pay selected" are executed INSIDE the `GET /commission_cashback/payment/rows`
      listing request (rows L156-176): the JS stuffs `pay_all_hdn=1` or a CSV of
      ids into `pay_selecteds_hdn` — two hidden carrier columns — as DataTables
      column-search values and redraws the table. There is no POST, no CSRF
      semantics, no confirmation, and a browser refresh re-fires the attempt;
      only the per-row `paid=0` re-check inside `pay()` stops it double-paying.
      This rebuild implements the evident intent instead: an explicit, confirmed
      action. Both bulk buttons open a confirmation naming the row count, the
      per-currency totals and the period span, and only the confirm executes.
      The per-row `Auth::user()->can('pay', $cp)` re-check and the "already paid
      rows are skipped" behaviour are kept exactly, and the two hidden carrier
      columns are documented rather than rendered.
      // <!-- SUGGESTION: move Pay all / Pay selected onto their own POST route
      //      with CSRF + a confirmation step, the way the single-row
      //      `payment.pay` POST already works, and keep `rows()` a pure read.
      //      Today a bookmark or a refresh of the rows URL can move money. -->
   2. `admin.commission_cashback.payment.search` (routes/admin.php:238) maps to
      `CommissionCashbackPaymentController@search`, a method that does not exist
      — hitting it errors. The live UI dodges it by pointing its select2 at
      `admin.commission_cashback.search` instead. The Commission-cashback filter
      here behaves as the working one does (scoped to the caller's skins,
      narrowed by the chosen Skins, "- All -" first).
      // <!-- SUGGESTION: delete the dead payment.search route; it is a live URL
      //      that can only ever 500. -->
   3. `admin.commission_cashback.payment.info` (L237) runs with NO authorize()
      call (controller L335-430) while every other action on the model is
      isadmin-gated. It only returns next-payment-date previews for the settings
      form, but it is an unauthenticated-by-policy endpoint on a money model.
      Surfaced in the gate note, not reproduced (it belongs to the settings
      screen's form).
      // <!-- SUGGESTION: add the missing authorize('viewAny') to info(). -->

   FAITHFUL ABSENCES (brief §3 — nothing added):
   - NO create. `'no_create' => true`: rows are generated by the cron
     `GET /cronjobs/payCommissionCashbacks` (routes/cronjobs.php:119 →
     CronsController::payCommissionCashbacks L181 → queued
     App\Jobs\PayCommissionCashbacks → CommissionCashback::pay($date)).
   - NO export. NO KPI figures. The rows JSON returns `total_unpaid_count` and
     `page_unpaid_count` purely to enable/disable the bulk buttons (JS
     drawCallback L83-89) — those two counts are shown here in the bulk bar,
     which is what they are for, and nothing else is totalled.
   - NO bulk edit and NO bulk delete — Pay selected / Pay all are the only bulk
     verbs the controller implements.
   - NO status enum beyond the `paid` boolean.

   LABELS. The page title comes from `main_label` `commissions_cashback_payment`
   and the column headers from backend.commission_cashback / base_amount /
   percentage_to_be_paid / calculated_amount / paid_by / paid_at — none of those
   keys exist in any committed lang file (runtime translations load from the
   gitignored storage/lang/), so the live screen renders raw keys. Same for the
   tab label `backend.payments`, the bulk labels `backend.select_all` /
   `backend.pay_selected` / `backend.pay_all` and the confirm strings
   `backend.pay_ticket_label` / `backend.title_pay`. Operator-facing labels are
   written here per the label policy and marked with a "label inferred" comment.
   What DOES resolve: `backend.payment_status_paid` = "Paid",
   `backend.settings` = "Settings", `backend.id` = "ID", `backend.amount`,
   `commissions.note` = "Note", `backend.extras.auto_pay` = "Auto Pay".

   Demo session = Super admin "admin" (user_level 0) — viewAny/view are
   isadmin() only, so no other role can see this screen at all. All new
   top-level names are hccp/Hccp/HCCP-prefixed except the required page
   component `CommissionCashbackPayments`.
   ==================================================================== */

const { useState: hccpUseState, useMemo: hccpUseMemo } = React;

/* ---------- deterministic PRNG (same shape as the other Host pages) ---------- */
const hccpRand = (seed) => () => {
  seed = (seed + 0x6D2B79F5) | 0;
  let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};

const hccpPad = (n) => String(n).padStart(2, "0");

/* number_format($v, 2) with comma decimals — the real cells print e.g.
   "1.234,56" (controller L20-44). Deliberately NOT hrsMoney(), whose en-US
   grouping would misreport what the live column looks like. */
const hccpNum = (v) => {
  const n = Number(v || 0);
  const [i, d] = Math.abs(n).toFixed(2).split(".");
  return `${n < 0 ? "-" : ""}${i.replace(/\B(?=(\d{3})+(?!\d))/g, ".")},${d}`;
};

const hccpDate = (ts) => { const d = new Date(ts); return `${hccpPad(d.getDate())}/${hccpPad(d.getMonth() + 1)}/${d.getFullYear()}`; };
const hccpDateTime = (ts) => { const d = new Date(ts); return `${hccpDate(ts)} ${hccpPad(d.getHours())}:${hccpPad(d.getMinutes())}:${hccpPad(d.getSeconds())}`; };
const hccpDateMin = (ts) => { const d = new Date(ts); return `${hccpDate(ts)} ${hccpPad(d.getHours())}:${hccpPad(d.getMinutes())}`; };

/* Frozen "now" so the generated ledger is identical on every load. */
const HCCP_NOW = new Date(2026, 7, 7, 11, 24, 8).getTime();
const HCCP_DAY = 86400000;

/* The acting operator. viewAny/view are isadmin() only (Policy L19-34), so the
   only session that can reach this screen is a Super Admin. */
const HCCP_ACTOR = { id: 1, username: "admin", level: 0 };

/* rows() scopes to users whose skin_id is in Auth()->user()->getSkinIDS()
   (controller L97-100) — for an admin that is every skin (User.php:114-131),
   so the whole list is in scope for this session. Ids/currencies continue the
   skin set used by the other Host settings pages. */
const HCCP_SKINS = [
  { id: 47, name: "win24hs", cur: "ARS" },
  { id: 52, name: "apostando365", cur: "PYG" },
  { id: 55, name: "apuestadepana", cur: "CLP" },
  { id: 58, name: "PlaySpin", cur: "BOB" },
  { id: 62, name: "Jokerenvivo", cur: "ARS" },
  { id: 68, name: "Tucasino", cur: "ARS" },
  { id: 70, name: "Jugaygana", cur: "ARS" },
];
const hccpSkin = (id) => HCCP_SKINS.find(s => s.id === id) || null;

/* Parent `commission_cashbacks` rows — the config the sibling Settings screen
   owns. Only the fields this screen reads are modelled: the name it joins for
   the Commission Cashback column, the skins that scope the filter's select2,
   the balance_type that decides WHICH wallet column pay() credits, and auto_pay
   (which is what produces a paid row with paid_by = null → "Auto Pay"). */
const HCCP_CASHBACKS = [
  { id: 3, name: "Weekly GGR cashback 10%", skins: [47, 68, 70], pct: 10, balance_type: "balance_withdrawable", auto_pay: false, periodicity: "weekly", days: 7 },
  { id: 5, name: "Monthly casino cashback 5%", skins: [52, 55], pct: 5, balance_type: "balance", auto_pay: true, periodicity: "monthly", days: 30 },
  { id: 8, name: "Live casino cashback 3%", skins: [58, 62], pct: 3, balance_type: "balance", auto_pay: false, periodicity: "weekly", days: 7 },
  { id: 11, name: "VIP weekly cashback 12%", skins: [47, 62, 70], pct: 12, balance_type: "balance_withdrawable", auto_pay: true, periodicity: "weekly", days: 7 },
];
const hccpCashback = (id) => HCCP_CASHBACKS.find(c => c.id === id) || null;

/* balance_type labels resolve for real (backend.php:209-210). */
const hccpBalanceLabel = (t) => t === "balance_withdrawable" ? "Withdrawable balance" : "Non withdrawable balance";

/* getPayerLabel() — CommissionCashbackPayment Model L40-48:
   paid + paid_by null → "Auto Pay"; paid + paid_by → payer username (fallback
   "Payer not available"); unpaid → blank. */
const hccpPayerLabel = (row) => {
  if (!row.paid) return "";
  if (row.paid_by == null) return "Auto Pay";
  return row.payer || "Payer not available";
};

const HCCP_PAGE_SIZES = [5, 10, 25, 50];

/* ---------- rows ----------
   Real columns (migration 2026_06_24_155202_create_commission_cashback_payment_table):
   cc_id, user_id, paid, paid_by, paid_at, from, to, base_amount,
   calculated_amount, min/max_base_amount, cashback_percentage, edited_by,
   edit_reason. base_amount is SUM(players_report.profit) over the period (GGR),
   clipped by max_base_amount and gated by min_base_amount, and
   calculated_amount = base_amount × cashback_percentage / 100 unless an admin
   edited it — an edited row also blocks the cron from regenerating that
   player/period (CommissionCashback::pay L300-308).
   Two rows deliberately carry a broken relation so the controller's real
   fallbacks ("Skin not available", "User not available") are visible. */
const hccpGenRows = () => {
  const r = hccpRand(0x5cb02026);
  const rows = [];
  let id = 40318;
  const names = ["marianoleon", "sofiacruz", "elbertogomez", "rominaf", "lucascabrera", "nadiaperez", "javigimenez", "carladuarte",
    "pablo_mza", "vaneriquelme", "hugoacosta", "milagrosr", "brunosalas", "danielaq", "ivanmoreno", "yaninatorres",
    "rodrigovera", "camilaod", "matiasleiva", "fernandaz", "gonzaloibarra", "luzmarina", "kevinaguero", "noeliab"];
  const payers = ["admin", "adminfinanzas", "admin", "adminops"];
  const reasons = [
    "Base amount recalculated after a voided casino session.",
    "Manual correction agreed with the affiliate — period overlap with the previous run.",
    "Capped to the agreed monthly ceiling for this player.",
  ];

  /* Newest period first, walking backwards one period at a time per cashback. */
  for (let p = 0; p < 6; p++) {
    HCCP_CASHBACKS.forEach((cc) => {
      const span = cc.days * HCCP_DAY;
      const to = HCCP_NOW - p * span - HCCP_DAY;
      const from = to - span + HCCP_DAY;
      const perPeriod = 2 + Math.floor(r() * 3);
      for (let k = 0; k < perPeriod; k++) {
        const skinId = cc.skins[Math.floor(r() * cc.skins.length)];
        const skin = hccpSkin(skinId);
        const uname = names[Math.floor(r() * names.length)];
        const base = Math.round((2000 + r() * 480000) * 100) / 100;
        const calcRaw = Math.round(base * cc.pct) / 100;
        /* How a row ends up paid, per CommissionCashback::pay(): a cashback
           with auto_pay set has CommissionCashbackPayment::pay() fired for it
           the moment the cron generates it, so those rows are always paid and
           always carry paid_by = null → "Auto Pay". A cashback without auto_pay
           only ever gets paid from this screen, so its rows sit unpaid until an
           operator works them — older periods have mostly been settled by hand,
           the recent ones are the backlog this screen exists to clear. */
        const settled = cc.auto_pay || (p >= 3 ? r() < 0.88 : r() < 0.22);
        const edited = !settled && r() < 0.16;
        const paidAt = settled ? to + HCCP_DAY + Math.floor(r() * 8) * 3600000 : null;
        rows.push({
          id: id--,
          cc_id: cc.id,
          user_id: 4600000 + Math.floor(r() * 180000),
          username: uname,
          skin_id: skinId,
          currency: skin ? skin.cur : "",
          created_at: to + HCCP_DAY + Math.floor(r() * 3) * 3600000,
          from,
          to,
          base,
          pct: cc.pct,
          calc: edited ? Math.round(calcRaw * 0.82 * 100) / 100 : calcRaw,
          min_base: 5000,
          max_base: 4000000,
          paid: settled,
          paid_by: settled ? (cc.auto_pay ? null : 1) : null,
          payer: settled && !cc.auto_pay ? payers[Math.floor(r() * payers.length)] : null,
          paid_at: paidAt,
          edited_by: edited ? "adminfinanzas" : null,
          edit_reason: edited ? reasons[Math.floor(r() * reasons.length)] : null,
          updated_at: edited ? to + HCCP_DAY + 5 * 3600000 : null,
        });
      }
    });
  }

  /* The two documented fallback cases: a payment whose user row is gone
     ("User not available") and one whose skin cannot be resolved
     ("Skin not available"). Applied to unpaid rows so both stay payable and the
     confirmation has to cope with a missing recipient name. */
  const unpaidIdx = rows.map((row, i) => (row.paid ? -1 : i)).filter(i => i >= 0);
  if (unpaidIdx.length > 1) {
    rows[unpaidIdx[0]] = { ...rows[unpaidIdx[0]], username: null, user_id: null };
    rows[unpaidIdx[1]] = { ...rows[unpaidIdx[1]], skin_id: null, currency: "" };
  }
  return rows;
};

const HCCP_SEED_ROWS = hccpGenRows();

/* ---------- sorting ----------
   rows L135-153: id, from, to, base_amount, calculated_amount, paid_at direct;
   date→created_at; percentage_to_be_paid→cashback_percentage;
   payment_status_paid→paid; commission_cashback→commission_cashbacks.name;
   skin→skins.name; paid_by→payer.username (leftJoin alias);
   username→users.username. Default commission_cashback_payment.id DESC. */
const HCCP_SORTERS = {
  id: (r) => r.id,
  commission_cashback: (r) => (hccpCashback(r.cc_id) || { name: "" }).name.toLowerCase(),
  skin: (r) => (hccpSkin(r.skin_id) || { name: "" }).name.toLowerCase(),
  username: (r) => (r.username || "").toLowerCase(),
  date: (r) => r.created_at,
  from: (r) => r.from,
  to: (r) => r.to,
  base_amount: (r) => r.base,
  percentage_to_be_paid: (r) => r.pct,
  calculated_amount: (r) => r.calc,
  payment_status_paid: (r) => (r.paid ? 1 : 0),
  paid_by: (r) => hccpPayerLabel(r).toLowerCase(),
  paid_at: (r) => r.paid_at || 0,
};

/* ---------- filters ----------
   filters/commissions_cashback_payment.blade.php: ID (=), Commission cashback
   (select2 → cc_id =), Skins (multi → skins.id IN csv), Date (d/m/Y range →
   created_at BETWEEN start 00:00:00 AND end 23:59:59 via sistemadata()),
   Paid (Select / yes / no → paid =). */
const HCCP_BLANK_FILTERS = { id: "", cc: "", skins: [], date: { from: "", to: "" }, paid: "" };

const hccpFilterRows = (rows, f) => rows.filter((row) => {
  if (f.id && String(row.id) !== String(f.id).trim()) return false;
  if (f.cc && String(row.cc_id) !== String(f.cc)) return false;
  if (f.skins && f.skins.length && !f.skins.includes(String(row.skin_id))) return false;
  if (f.paid === "yes" && !row.paid) return false;
  if (f.paid === "no" && row.paid) return false;
  const d = f.date || {};
  if (d.from) { const t = new Date(`${d.from}T00:00:00`).getTime(); if (row.created_at < t) return false; }
  if (d.to) { const t = new Date(`${d.to}T23:59:59`).getTime(); if (row.created_at > t) return false; }
  return true;
});

/* Per-currency totals — used only inside the bulk confirmation so the operator
   sees exactly what a bulk pay will move. Not a screen KPI: the real page
   displays no totals at all. */
const hccpTotalsByCur = (rows) => {
  const m = {};
  /* A row whose skin cannot be resolved has no currency to report — the
     payment table stores none of its own. Kept visible rather than defaulted
     to a currency the row does not actually have. */
  rows.forEach(r => { const c = r.currency || ""; m[c] = (m[c] || 0) + r.calc; });
  return Object.keys(m).sort().map(c => ({ cur: c, label: c || "(currency unknown)", total: m[c] }));
};

/* ==================================================================
   Settings | Payments tab bar — real, and rendered above BOTH screens
   (filters/commission_cashback.blade.php:13-18 and
   filters/commissions_cashback_payment.blade.php:13-18).
   ================================================================== */
const HccpTabs = ({ onSettings }) => (
  <div className="hccp-tabs" role="tablist">
    <button type="button" role="tab" aria-selected="false" className="hccp-tab" onClick={onSettings}>
      Settings{/* backend.settings — resolves for real */}
    </button>
    <button type="button" role="tab" aria-selected="true" className="hccp-tab hccp-tab--on">
      Payments{/* label inferred — backend.payments is absent from committed lang */}
    </button>
    <span className="hccp-tabs__note">
      Settings holds the cashback rules — periodicity, days, percentage, balance type, auto-pay.
      <Tip size={12}>
        The two screens share this tab bar in the real admin. Settings is <code>GET /commission_cashback</code>{" "}
        (<code>Admin\CommissionCashbackController</code>), a separate page; this tab is the payments ledger it
        produces. Both are URL-only — the sidebar entry that would link them is commented out.
      </Tip>
    </span>
  </div>
);

/* ==================================================================
   Bulk bar — Select all / Pay selected / Pay all.
   Enablement mirrors the real drawCallback (JS L83-89), which toggles the
   buttons from `page_unpaid_count` / `total_unpaid_count` in the rows JSON.
   ================================================================== */
const HccpBulkBar = ({ pageUnpaid, totalUnpaid, selectedCount, allPageSelected, onSelectAll, onPaySelected, onPayAll }) => (
  <div className="hccp-bulk">
    <div className="hccp-bulk__counts">
      <span className="hccp-bulk__c"><b>{hrsInt(totalUnpaid)}</b> unpaid in the filtered set</span>
      <span className="hccp-bulk__sep" />
      <span className="hccp-bulk__c"><b>{hrsInt(pageUnpaid)}</b> unpaid on this page</span>
      <Tip size={12}>
        These are exactly the two numbers the real <code>rows()</code> response carries —{" "}
        <code>total_unpaid_count</code> and <code>page_unpaid_count</code> — and their only job there is to
        enable or disable these buttons. The screen shows no other totals, so none were added.
      </Tip>
    </div>
    <div className="hccp-bulk__btns">
      <button type="button" className="rpt-btn hccp-btn hccp-btn--ghost" disabled={!pageUnpaid} onClick={onSelectAll}>
        <Icon name={allPageSelected ? "check" : "list"} size={13} />
        {allPageSelected ? "Clear selection" : "Select all"}{/* label inferred — backend.select_all */}
      </button>
      <button type="button" className="rpt-btn rpt-btn--green hccp-btn" disabled={!selectedCount} onClick={onPaySelected}>
        <Icon name="wallet" size={13} /> Pay selected{selectedCount ? ` (${selectedCount})` : ""}{/* label inferred — backend.pay_selected */}
      </button>
      <button type="button" className="rpt-btn rpt-btn--danger hccp-btn" disabled={!totalUnpaid} onClick={onPayAll}>
        <Icon name="zap" size={13} /> Pay all{/* label inferred — backend.pay_all */}
      </button>
      <Tip size={12}>
        <b>Select all</b> ticks the payable rows on <i>this page</i>; <b>Pay selected</b> pays the ticked ids;{" "}
        <b>Pay all</b> pays every unpaid row matching the <i>current filter</i> — not just this page. On the real
        screen all three run as hidden-column side effects of the <code>GET …/rows</code> listing request, with no
        confirmation; here each one asks first. Already-paid rows are skipped either way.
      </Tip>
    </div>
  </div>
);

/* ==================================================================
   Pay confirmation — the unambiguous step this screen does not have in
   production. Always names the amount, the recipient and the period, and
   refuses to offer a confirm for rows that are already paid.
   ================================================================== */
const HccpPayModal = ({ payload, onClose, onConfirm }) => {
  if (!payload) return null;
  const { rows, scope } = payload;
  const payable = rows.filter(r => !r.paid);
  const skipped = rows.length - payable.length;
  const single = scope === "row";
  const one = payable[0];
  const totals = hccpTotalsByCur(payable);
  const spanFrom = payable.length ? Math.min(...payable.map(r => r.from)) : 0;
  const spanTo = payable.length ? Math.max(...payable.map(r => r.to)) : 0;
  const scopeLabel = scope === "all" ? "Pay all — every unpaid row matching the current filter"
    : scope === "selected" ? "Pay selected — the rows ticked on this page"
      : "Single payment";

  return (
    <div className="bp-modal-scrim hccp-scrim" onClick={onClose}>
      <div className="hccp-modal hccp-modal--pay" onClick={(e) => e.stopPropagation()}>
        <div className="hccp-modal__head">
          <div className="hccp-modal__ic hccp-modal__ic--money"><Icon name="wallet" size={17} /></div>
          <div className="hccp-modal__title">
            {single ? "Confirm cashback payment" : "Confirm bulk cashback payment"}
            {/* label inferred — backend.title_pay / backend.pay_ticket_label */}
            <span className="hccp-modal__sub">{scopeLabel}</span>
          </div>
          <button className="hccp-modal__x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>

        <div className="hccp-modal__body">
          {payable.length === 0 ? (
            <div className="hccp-verdict hccp-verdict--muted">
              <Icon name="info" size={13} />
              <span>Every row in this selection is already paid. Nothing will be moved — <code>pay()</code> re-checks{" "}
                <code>paid</code> inside the row lock and returns without crediting.</span>
            </div>
          ) : single ? (
            <React.Fragment>
              <div className="hccp-payhero">
                <div className="hccp-payhero__l">Amount to pay</div>
                <div className="hccp-payhero__v">{hccpNum(one.calc)} <span className="hccp-payhero__c">{one.currency || "(currency unknown)"}</span></div>
                <div className="hccp-payhero__to">
                  to <b>{one.username || "User not available"}</b>
                  {one.user_id ? <span className="hccp-mono"> · id {one.user_id}</span> : null}
                  {hccpSkin(one.skin_id) ? <span className="hccp-mono"> · {hccpSkin(one.skin_id).name}</span> : <span className="hccp-mono"> · Skin not available</span>}
                </div>
              </div>
              <div className="hccp-kv">
                <span>Period</span><b>{hccpDate(one.from)} → {hccpDate(one.to)}</b>
                <span>Commission cashback</span><b>{(hccpCashback(one.cc_id) || { name: "—" }).name}</b>
                <span>Base amount (GGR)</span><b>{hccpNum(one.base)} {one.currency}</b>
                <span>Percentage to be paid</span><b>{one.pct} %</b>
                <span>Credited to</span><b>{hccpBalanceLabel((hccpCashback(one.cc_id) || {}).balance_type)}</b>
                <span>Payment id</span><b className="hccp-mono">#{one.id}</b>
              </div>
            </React.Fragment>
          ) : (
            <React.Fragment>
              <div className="hccp-payhero">
                <div className="hccp-payhero__l">{hrsInt(payable.length)} payments will be made</div>
                <div className="hccp-payhero__v">
                  {totals.map((t, i) => (
                    <span key={t.label} className="hccp-payhero__tot">
                      {i > 0 && <span className="hccp-payhero__plus">+</span>}
                      {hccpNum(t.total)} <span className="hccp-payhero__c">{t.label}</span>
                    </span>
                  ))}
                </div>
                <div className="hccp-payhero__to">
                  covering periods <b>{hccpDate(spanFrom)} → {hccpDate(spanTo)}</b>
                </div>
              </div>
              <div className="hccp-preview">
                <div className="hccp-preview__cap">Recipients</div>
                <div className="hccp-preview__scroll">
                  <table className="hccp-preview__t">
                    <thead><tr><th>ID</th><th>Username</th><th>Period</th><th className="hccp-r">Amount</th></tr></thead>
                    <tbody>
                      {payable.slice(0, 12).map(r => (
                        <tr key={r.id}>
                          <td className="hccp-mono">#{r.id}</td>
                          <td>{r.username || "User not available"}</td>
                          <td>{hccpDate(r.from)} → {hccpDate(r.to)}</td>
                          <td className="hccp-r"><b>{hccpNum(r.calc)}</b> {r.currency}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
                {payable.length > 12 && <div className="hccp-preview__more">+ {hrsInt(payable.length - 12)} more rows in this batch.</div>}
              </div>
              {/* Same divergence note the sibling Commission Payments screen carries — both
                  screens ship the identical GET-side-effect defect upstream. */}
              <div className="hccp-verdict hccp-verdict--muted">
                <Icon name="alert" size={13} />
                <span>
                  <b>Divergence from the real platform.</b> Upstream this bulk action has no confirmation and no
                  POST: the JS writes <code>pay_all_hdn</code> / <code>pay_selecteds_hdn</code> into hidden
                  DataTables columns and the payments execute inside the{" "}
                  <code>GET …/payment/rows</code> listing request, so refreshing that URL re-fires the attempt.
                  This step is the evident intent, not the shipped behaviour.
                </span>
              </div>
            </React.Fragment>
          )}

          {skipped > 0 && (
            <div className="hccp-verdict hccp-verdict--muted">
              <Icon name="info" size={13} />
              <span><b>{hrsInt(skipped)}</b> already-paid {skipped === 1 ? "row is" : "rows are"} in this selection and will be skipped — never paid twice.</span>
            </div>
          )}

          {payable.length > 0 && (
            <div className="hccp-verdict hccp-verdict--ok">
              <Icon name="shield" size={13} />
              <span>
                <code>CommissionCashbackPayment::pay()</code> runs inside a DB transaction with the row{" "}
                <code>lockForUpdate</code> and re-checks <code>paid</code> under the lock, so a double submit
                cannot pay twice.
              </span>
            </div>
          )}

          {payable.length > 0 && (
            <p className="hccp-modal__note hccp-modal__note--last">
              On confirm each row is credited to the player&rsquo;s wallet column, stamped{" "}
              <code>paid = 1</code> / <code>paid_by = {HCCP_ACTOR.username}</code> / <code>paid_at</code>, and a
              history row is written with <code>transaction_type = 121</code> (COMMISSION_CASHBACK) and{" "}
              <code>res_type = 6</code>. The policy is re-checked per row before each payment, exactly as the
              server does.
            </p>
          )}
        </div>

        <div className="hccp-modal__foot">
          <button className="rpt-btn hccp-btn hccp-btn--ghost" onClick={onClose}>Cancel</button>
          {payable.length > 0 && (
            <button className="rpt-btn rpt-btn--green hccp-btn" onClick={() => onConfirm(payable)}>
              <Icon name="check" size={13} />
              {single
                ? `Pay ${hccpNum(one.calc)}${one.currency ? " " + one.currency : ""} to ${one.username || "this player"}`
                : `Pay ${hrsInt(payable.length)} payments`}
            </button>
          )}
        </div>
      </div>
    </div>
  );
};

/* ==================================================================
   Edit / View modal — models/commission_cashback_payment.blade.php.
   Editable only when act == 'edit' && !paid (blade L52-64); a paid row opens
   the same modal read-only (the real screen swaps Edit for View).
   ================================================================== */
const HccpEditModal = ({ row, mode, onClose, onSave }) => {
  const editable = mode === "edit";
  const [amount, setAmount] = hccpUseState(row ? String(row.calc) : "");
  const [reason, setReason] = hccpUseState("");
  const [errs, setErrs] = hccpUseState([]);
  if (!row) return null;
  const cc = hccpCashback(row.cc_id);
  const skin = hccpSkin(row.skin_id);

  /* update() L286-289: calculated_amount required|numeric|gt:0,
     edit_reason required|string. Errors come back as ajaxError + campierrati. */
  const submit = () => {
    const e = [];
    const n = Number(String(amount).replace(",", "."));
    if (amount === "" || Number.isNaN(n)) e.push("Amount is required and must be a number.");
    else if (!(n > 0)) e.push("Amount must be greater than 0.");
    if (!reason.trim()) e.push("Note is required.");
    setErrs(e);
    if (e.length) return;
    onSave(row, n, reason.trim());
  };

  return (
    <div className="bp-modal-scrim hccp-scrim" onClick={onClose}>
      <div className="hccp-modal" onClick={(e) => e.stopPropagation()}>
        <div className="hccp-modal__head">
          <div className="hccp-modal__ic"><Icon name={editable ? "edit" : "eye"} size={16} /></div>
          <div className="hccp-modal__title">
            {editable ? "Edit payment" : "View payment"}
            <span className="hccp-modal__sub">#{row.id} · {row.username || "User not available"}</span>
          </div>
          <button className="hccp-modal__x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>

        <div className="hccp-modal__body">
          <div className="hccp-kv">
            <span>Skin</span><b>{skin ? skin.name : "Skin not available"}</b>
            <span>Username</span><b>{row.username || "User not available"}</b>
            <span>Commission cashback</span><b>{cc ? cc.name : "—"}</b>{/* label inferred */}
            <span>Date</span><b>{hccpDateTime(row.created_at)}</b>
            <span>Period</span><b>{hccpDate(row.from)} → {hccpDate(row.to)}</b>
            <span>Base amount</span><b>{hccpNum(row.base)} {row.currency}</b>{/* label inferred */}
            <span>Percentage to be paid</span><b>{row.pct} %</b>{/* label inferred */}
            <span>Calculated amount</span><b>{hccpNum(row.calc)} {row.currency}</b>{/* label inferred */}
            <span>Paid</span><b>{row.paid ? "Yes" : "No"}</b>
            {row.paid && <React.Fragment>
              <span>Paid by</span><b>{hccpPayerLabel(row)}</b>{/* label inferred */}
              <span>Paid at</span><b>{row.paid_at ? hccpDateMin(row.paid_at) : "—"}</b>{/* label inferred */}
            </React.Fragment>}
            {row.edited_by && <React.Fragment>
              <span>Edit User</span><b>{row.edited_by}</b>
              <span>Updated At</span><b>{row.updated_at ? hccpDateMin(row.updated_at) : "—"}</b>
            </React.Fragment>}
          </div>

          {row.edit_reason && (
            <div className="hccp-audit">
              <div className="hccp-audit__h"><Icon name="info" size={12} /> Audit trail</div>
              <p className="hccp-audit__q">&ldquo;{row.edit_reason}&rdquo;</p>
              <p className="hccp-audit__n">
                <code>edited_by</code> + <code>edit_reason</code> persist on the row, and an edited payment blocks
                the <code>payCommissionCashbacks</code> cron from regenerating that player/period
                (<code>CommissionCashback::pay</code> L300-308) — the manual figure wins from here on.
              </p>
            </div>
          )}

          {editable ? (
            <div className="hccp-form">
              <label className="hccp-field">
                <span className="hccp-field__l">Amount <i>({row.currency || "player currency"})</i> <em>required</em></span>
                <input className="hccp-input" inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} />
                <span className="hccp-field__h">Writes <code>calculated_amount</code> · <code>required|numeric|gt:0</code></span>
              </label>
              <label className="hccp-field">
                <span className="hccp-field__l">Note <em>required</em></span>
                <textarea className="hccp-input hccp-input--area" rows={3} value={reason} onChange={(e) => setReason(e.target.value)}
                  placeholder="Why is this amount being changed?" />
                <span className="hccp-field__h">Writes <code>edit_reason</code> and stamps <code>edited_by</code> · <code>required|string</code></span>
              </label>
              {errs.length > 0 && (
                <div className="hccp-errs">
                  {errs.map((x, i) => <div key={i}><Icon name="alert" size={12} /> {x}</div>)}
                </div>
              )}
            </div>
          ) : (
            <div className="hccp-verdict hccp-verdict--muted">
              <Icon name="lock" size={13} />
              <span>Read-only. <code>update</code> is <code>isadmin() && !paid</code> (Policy L54-74) — a paid
                payment can no longer be edited or deleted, only viewed.</span>
            </div>
          )}
        </div>

        <div className="hccp-modal__foot">
          <button className="rpt-btn hccp-btn hccp-btn--ghost" onClick={onClose}>{editable ? "Cancel" : "Close"}</button>
          {editable && <button className="rpt-btn rpt-btn--blue hccp-btn" onClick={submit}><Icon name="check" size={13} /> Save</button>}
        </div>
      </div>
    </div>
  );
};

/* ==================================================================
   Delete confirmation — DELETE /commission_cashback/payment/{id},
   policy delete = isadmin() && !paid.
   ================================================================== */
const HccpDeleteModal = ({ row, onClose, onConfirm }) => {
  if (!row) return null;
  return (
    <div className="bp-modal-scrim hccp-scrim" onClick={onClose}>
      <div className="hccp-modal hccp-modal--sm" onClick={(e) => e.stopPropagation()}>
        <div className="hccp-modal__head">
          <div className="hccp-modal__ic hccp-modal__ic--danger"><Icon name="trash" size={16} /></div>
          <div className="hccp-modal__title">
            Delete payment
            <span className="hccp-modal__sub">#{row.id} · {row.username || "User not available"}</span>
          </div>
          <button className="hccp-modal__x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>
        <div className="hccp-modal__body">
          <p>Are you sure you want to delete this payment?</p>
          <div className="hccp-kv">
            <span>Period</span><b>{hccpDate(row.from)} → {hccpDate(row.to)}</b>
            <span>Calculated amount</span><b>{hccpNum(row.calc)} {row.currency}</b>
          </div>
          <p className="hccp-modal__note hccp-modal__note--last">
            Deleting removes the row only — no money has moved yet, because delete is refused once{" "}
            <code>paid</code> is set. Unless this row was edited, the next{" "}
            <code>payCommissionCashbacks</code> cron run can regenerate the same player/period.
          </p>
        </div>
        <div className="hccp-modal__foot">
          <button className="rpt-btn hccp-btn hccp-btn--ghost" onClick={onClose}>Cancel</button>
          <button className="rpt-btn rpt-btn--danger hccp-btn" onClick={() => onConfirm(row)}><Icon name="trash" size={13} /> Delete</button>
        </div>
      </div>
    </div>
  );
};

/* ==================================================================
   Honest-absence strip — what this screen genuinely does not have.
   ================================================================== */
const HccpAbsences = () => (
  <div className="hccp-absence">
    <div className="hccp-absence__h"><Icon name="info" size={13} /> Not on this screen</div>
    <ul className="hccp-absence__l">
      <li><b>No create.</b> Payments are produced by the <code>payCommissionCashbacks</code> cron from a{" "}
        <code>players_report</code> GGR aggregation, never by hand — the controller sets <code>no_create</code>.</li>
      <li><b>No export and no totals.</b> The generic layout loads the DataTables export bundles but configures no
        buttons, and the only counters the response carries are the two unpaid counts that gate the bulk buttons.</li>
      <li><b>No bulk edit or bulk delete.</b> Pay selected and Pay all are the only bulk verbs implemented.</li>
      <li><b>No status beyond <code>paid</code>.</b> There is no state machine here — a row is unpaid or paid, and
        paid is terminal: edit, delete and pay all refuse it.</li>
    </ul>
  </div>
);

/* ==================================================================
   Page component — name required by the orchestrator's route wiring.
   ================================================================== */
const CommissionCashbackPayments = ({ onNav }) => {
  window.useLocale && window.useLocale();

  const [rows, setRows] = hccpUseState(HCCP_SEED_ROWS);
  const [draft, setDraft] = hccpUseState(HCCP_BLANK_FILTERS);
  /* The generic DataTable loads on init, so the list is populated on arrival —
     this is not one of the no-auto-load report screens. */
  const [applied, setApplied] = hccpUseState(HCCP_BLANK_FILTERS);
  const [sort, setSort] = hccpUseState({ key: "id", dir: "desc" });
  const [page, setPage] = hccpUseState(0);
  const [pageSize, setPageSize] = hccpUseState(50); // JS pageLength 50, lengthMenu [5,10,25,50]
  const [sel, setSel] = hccpUseState({});
  const [payModal, setPayModal] = hccpUseState(null);
  const [editModal, setEditModal] = hccpUseState(null);
  const [delModal, setDelModal] = hccpUseState(null);

  const filtered = hccpUseMemo(() => hccpFilterRows(rows, applied), [rows, applied]);
  const sorted = hccpUseMemo(() => {
    const get = HCCP_SORTERS[sort.key] || HCCP_SORTERS.id;
    return [...filtered].sort((a, b) => {
      const av = get(a), bv = get(b);
      const d = typeof av === "string" ? av.localeCompare(bv) : av - bv;
      return sort.dir === "asc" ? d : -d;
    });
  }, [filtered, sort]);
  const view = sorted.slice(page * pageSize, (page + 1) * pageSize);

  /* The two counters the real rows() JSON returns. */
  const totalUnpaid = filtered.filter(r => !r.paid).length;
  const pageUnpaid = view.filter(r => !r.paid).length;
  const selectedRows = view.filter(r => sel[r.id] && !r.paid);
  const allPageSelected = pageUnpaid > 0 && selectedRows.length === pageUnpaid;

  /* Commission-cashback filter options, narrowed by the chosen Skins exactly as
     the working select2 (admin.commission_cashback.search) does, with the
     " - All -" first row it optionally emits. */
  const ccOptions = hccpUseMemo(() => {
    const chosen = (draft.skins || []).map(Number);
    const pool = chosen.length ? HCCP_CASHBACKS.filter(c => c.skins.some(s => chosen.includes(s))) : HCCP_CASHBACKS;
    return pool.map(c => ({ value: String(c.id), label: c.name }));
  }, [draft.skins]);

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "Exact id", width: 130,
      tip: <>Exact match on <code>commission_cashback_payment.id</code>.</> },
    { key: "cc", label: "Commission cashback", type: "select", placeholder: "- All -", options: ccOptions, grow: true,
      tip: <>Filters <code>cc_id</code>. The live select2 is scoped to the caller&rsquo;s{" "}
        <code>getSkinIDS()</code> and narrows to the Skins picked here. It points at{" "}
        <code>admin.commission_cashback.search</code> because the payments tab&rsquo;s own{" "}
        <code>payment.search</code> route maps to a controller method that does not exist.</> },
    { key: "skins", label: "Skins", type: "multi", options: HCCP_SKINS.map(s => ({ value: String(s.id), label: s.name })), placeholder: "- All -",
      tip: <>Matches <code>skins.id IN (…)</code> — the skin comes from the paid player&rsquo;s{" "}
        <code>users.skin_id</code>, not from the cashback rule.</> },
    { key: "date", label: "Date", type: "daterange", icon: "calendar",
      tip: <>Filters <code>created_at</code> — the moment the cron generated the payment row, not the period it
        covers. The range is inclusive: <code>from 00:00:00</code> → <code>to 23:59:59</code>.</> },
    { key: "paid", label: "Paid", type: "select", placeholder: "- Select -", options: [{ value: "yes", label: "Yes" }, { value: "no", label: "No" }], width: 150 },
  ];

  const resetFilters = () => { setDraft(HCCP_BLANK_FILTERS); setApplied(HCCP_BLANK_FILTERS); setPage(0); setSel({}); };

  /* ---------- actions ----------
     Every one of these is policy-gated on the real platform:
     pay/update/delete = isadmin() && !paid (Policy L54-74). The session here is
     Super Admin, so the gate that actually varies per row is `!paid`. */
  const canPay = (r) => !r.paid;
  const canEdit = (r) => !r.paid;
  const canDelete = (r) => !r.paid;

  /* pay() — the idempotent claim. Rows already paid are skipped, never repaid;
     that is the model's own re-check under the row lock, kept here so a stale
     selection can never double-pay. */
  const runPay = (targets) => {
    const ids = new Set(targets.map(t => t.id));
    /* Read the live rows, not the snapshot the modal was opened with — a
       selection can go stale, and an already-paid row must fall out here
       exactly as pay() drops it under the row lock. */
    const inScope = rows.filter(r => ids.has(r.id));
    const paying = inScope.filter(r => !r.paid);
    const skipped = inScope.length - paying.length;
    const stamp = Date.now();
    setRows(rs => rs.map(r => (ids.has(r.id) && !r.paid)
      ? { ...r, paid: true, paid_by: HCCP_ACTOR.id, payer: HCCP_ACTOR.username, paid_at: stamp }
      : r));
    setSel({});
    setPayModal(null);
    const sum = hccpTotalsByCur(paying).map(t => `${hccpNum(t.total)} ${t.label}`).join(" + ");
    hrsToast(
      paying.length === 1 ? "Cashback payment made" : `${hrsInt(paying.length)} cashback payments made`,
      `${sum || "Nothing moved"} credited · paid_by = ${HCCP_ACTOR.username} · history rows written with transaction_type 121 (COMMISSION_CASHBACK), res_type 6.${skipped ? ` ${skipped} already-paid row(s) skipped — never paid twice.` : ""}`
    );
  };

  const runSave = (row, amount, reason) => {
    setRows(rs => rs.map(r => r.id === row.id
      ? { ...r, calc: amount, edited_by: HCCP_ACTOR.username, edit_reason: reason, updated_at: Date.now() }
      : r));
    setEditModal(null);
    hrsToast(`Payment #${row.id} updated`,
      `calculated_amount = ${hccpNum(amount)} ${row.currency} · edited_by stamped. The cron will now skip this player/period instead of regenerating it.`);
  };

  const runDelete = (row) => {
    setRows(rs => rs.filter(r => r.id !== row.id));
    setDelModal(null);
    setSel(s => { const n = { ...s }; delete n[row.id]; return n; });
    hrsToast(`Payment #${row.id} deleted`, `Unpaid row removed — no money had moved. ${row.edited_by ? "It was an edited row, so the cron will not recreate it." : "The next cron run can regenerate this player/period."}`);
  };

  const toggleSel = (r) => setSel(s => ({ ...s, [r.id]: !s[r.id] }));
  const selectAllPage = () => {
    if (allPageSelected) { setSel({}); return; }
    const next = {};
    view.forEach(r => { if (!r.paid) next[r.id] = true; });
    setSel(next);
  };

  /* The Settings half of the tab bar leads to the sibling config screen
     (component `CommissionCashback`, route key `comm-cashback`). If the host
     passes an onNav it is used; otherwise the page is rendered without one, so
     fall back to pushing the sibling's registered path and letting app.jsx's
     own popstate handler resolve it — no reaching into the router from here. */
  const openSettings = () => {
    if (onNav) { onNav("comm-cashback"); return; }
    try {
      const path = (window.pathForActive && window.pathForActive("comm-cashback")) || "/commissions/cashback";
      window.history.pushState({ active: "comm-cashback" }, "", path);
      const ev = typeof PopStateEvent === "function" ? new PopStateEvent("popstate") : new Event("popstate");
      window.dispatchEvent(ev);
      return;
    } catch (_e) { /* fall through to the honest note below */ }
    hrsToast("Settings tab", "The Commission cashback settings screen is its own page (GET /commission_cashback).");
  };

  /* The Username cell links to the player. On the real platform that is the
     per-player detail page (`admin.players.show` → GET /players/{id}/); this
     build's counterpart is the host Players screen (route key `host-players` →
     /players, src/pages/HostPlayers.jsx), which keeps its selected player in
     memory and has no per-player deep link. So the cell performs the real
     cross-page navigation to Players — same mechanism as openSettings above —
     and its title names the row's true target instead of a toast pretending a
     page opened. */
  const openPlayerScreen = () => {
    if (onNav) { onNav("host-players"); return; }
    const path = (window.pathForActive && window.pathForActive("host-players")) || "/players";
    try {
      if (window.location.pathname !== path) window.history.pushState({ active: "host-players" }, "", path);
      const ev = typeof PopStateEvent === "function" ? new PopStateEvent("popstate") : new Event("popstate");
      window.dispatchEvent(ev);
    } catch (_e) {
      window.location.href = path; // last resort: a real full-page load of the same path
    }
  };

  /* ---------- columns ----------
     Display order per the controller's ctor L29-44. The two hidden carrier
     columns `pay_all_hdn` / `pay_selecteds_hdn` are NOT rendered: they exist
     only to smuggle bulk-pay instructions into the listing GET, which this
     rebuild replaces with a confirmed action (divergence 1). */
  const COLUMNS = [
    {
      key: "id", label: "ID", sortable: true, width: 118, firstDir: "desc",
      /* The real table puts the per-row checkbox in the ID column (JS
         columnDefs L68-80) and only for rows that can be paid.
         // UNCLEAR: the reference calls them "payable rows", but this table has
         //   no `payable` column (that is bonus_payments / commissions_payments).
         //   The only per-row gate it names is the `pay` policy — isadmin() &&
         //   !paid — so the checkbox renders on exactly the unpaid rows. */
      render: (r) => (
        <span className="hccp-idcell">
          {canPay(r) ? (
            <input type="checkbox" className="hccp-check" checked={!!sel[r.id]} onChange={() => toggleSel(r)}
              aria-label={`Select payment ${r.id}`} onClick={(e) => e.stopPropagation()} />
          ) : <span className="hccp-check-gap" title="Paid rows cannot be selected — they can never be paid again" />}
          <span className="hccp-mono">#{r.id}</span>
        </span>
      ),
    },
    /* label inferred */
    { key: "commission_cashback", label: "Commission cashback", sortable: true,
      render: (r) => { const cc = hccpCashback(r.cc_id); return cc ? <span className="hccp-cc">{cc.name}{cc.auto_pay && <span className="hccp-auto" title="This cashback has auto_pay set — the cron settles its payments by itself, which is what produces an 'Auto Pay' payer">auto</span>}</span> : <span className="hccp-none">—</span>; } },
    { key: "skin", label: "Skin", sortable: true, width: 140,
      render: (r) => { const s = hccpSkin(r.skin_id); return s ? s.name : <span className="hccp-na" title="The controller prints this literal when the payment's user has no resolvable skin">Skin not available</span>; } },
    { key: "username", label: "Username", sortable: true, width: 150,
      render: (r) => r.username
        ? <a className="hccp-player"
            href={(window.pathForActive && window.pathForActive("host-players")) || "/players"}
            title={`Opens the Players screen. On the real platform this cell links to /players/${r.user_id}/ (admin.players.show) — this build's Players list has no per-player deep link, so it opens the list.`}
            onClick={(e) => { e.preventDefault(); openPlayerScreen(); }}>{r.username}</a>
        : <span className="hccp-na" title="The controller prints this literal when the payment's user row cannot be resolved">User not available</span> },
    { key: "date", label: "Date", sortable: true, width: 158, render: (r) => <span className="hccp-mono">{hccpDateTime(r.created_at)}</span> },
    { key: "from", label: "From", sortable: true, width: 108, render: (r) => <span className="hccp-mono">{hccpDate(r.from)}</span> },
    { key: "to", label: "To", sortable: true, width: 108, render: (r) => <span className="hccp-mono">{hccpDate(r.to)}</span> },
    /* label inferred */
    { key: "base_amount", label: "Base amount", align: "right", sortable: true, width: 132, render: (r) => <span className="hccp-amt">{hccpNum(r.base)}</span> },
    /* label inferred — the live cell prints the bare cashback_percentage value; the % is a unit marker only */
    { key: "percentage_to_be_paid", label: "%", align: "right", sortable: true, width: 78, render: (r) => <span className="hccp-amt">{r.pct}<span className="hccp-unit">%</span></span> },
    /* label inferred */
    { key: "calculated_amount", label: "Calculated amount", align: "right", sortable: true, width: 168,
      render: (r) => (
        <span className="hccp-calc">
          <b>{hccpNum(r.calc)}</b> <span className="hccp-unit">{r.currency || "—"}</span>
          {r.edited_by && <span className="hccp-edited" title={`Edited by ${r.edited_by} — an edited payment blocks the cron from regenerating this player/period`}>edited</span>}
        </span>
      ) },
    { key: "payment_status_paid", label: "Paid", align: "center", sortable: true, width: 96,
      render: (r) => r.paid
        ? <span className="hccp-pill hccp-pill--paid"><Icon name="check" size={10} /> Yes</span>
        : <span className="hccp-pill hccp-pill--topay">No</span> },
    /* label inferred */
    { key: "paid_by", label: "Paid by", sortable: true, width: 140,
      render: (r) => { const l = hccpPayerLabel(r); return l ? <span className={l === "Auto Pay" ? "hccp-autopay" : ""}>{l}</span> : <span className="hccp-none">—</span>; } },
    /* label inferred */
    { key: "paid_at", label: "Paid at", sortable: true, width: 138,
      render: (r) => r.paid_at ? <span className="hccp-mono">{hccpDateMin(r.paid_at)}</span> : <span className="hccp-none">—</span> },
    {
      key: "actions", label: "Actions", align: "center", width: 168,
      render: (r) => (
        <span className="hccp-acts">
          {canPay(r) && (
            <button className="hccp-act hccp-act--pay" title="Pay" onClick={(e) => { e.stopPropagation(); setPayModal({ rows: [r], scope: "row" }); }}>
              <Icon name="wallet" size={12} /> Pay
            </button>
          )}
          <button className="hccp-act" title={canEdit(r) ? "Edit" : "View — a paid payment is read-only"}
            onClick={(e) => { e.stopPropagation(); setEditModal({ row: r, mode: canEdit(r) ? "edit" : "view" }); }}>
            <Icon name={canEdit(r) ? "edit" : "eye"} size={12} />
          </button>
          {canDelete(r) && (
            <button className="hccp-act hccp-act--del" title="Delete" onClick={(e) => { e.stopPropagation(); setDelModal(r); }}>
              <Icon name="trash" size={12} />
            </button>
          )}
        </span>
      ),
    },
  ];

  /* Mobile card — the fields an operator scans first (who, how much, paid?),
     the rest behind the expander. Brief §11. */
  const renderCard = (r) => {
    const cc = hccpCashback(r.cc_id);
    return (
      <React.Fragment>
        <div className="hrs-card__top">
          <b>{r.username || "User not available"}</b>
          <span className="hccp-card__amt">{hccpNum(r.calc)} {r.currency}</span>
        </div>
        <div className="hccp-card__meta">
          {r.paid
            ? <span className="hccp-pill hccp-pill--paid"><Icon name="check" size={10} /> Paid</span>
            : <span className="hccp-pill hccp-pill--topay">To pay</span>}
          <span className="hccp-mono">#{r.id}</span>
          <span>{hccpDate(r.from)} → {hccpDate(r.to)}</span>
        </div>
        <div className="hrs-card__grid">
          <span>Commission cashback</span><b>{cc ? cc.name : "—"}</b>
          <span>Skin</span><b>{hccpSkin(r.skin_id) ? hccpSkin(r.skin_id).name : "Skin not available"}</b>
        </div>
        <details className="hccp-card__more">
          <summary>More</summary>
          <div className="hrs-card__grid">
            <span>Date</span><b>{hccpDateTime(r.created_at)}</b>
            <span>Base amount</span><b>{hccpNum(r.base)} {r.currency}</b>
            <span>Percentage</span><b>{r.pct} %</b>
            <span>Paid by</span><b>{hccpPayerLabel(r) || "—"}</b>
            <span>Paid at</span><b>{r.paid_at ? hccpDateMin(r.paid_at) : "—"}</b>
            {r.edited_by && <React.Fragment><span>Edited by</span><b>{r.edited_by}</b></React.Fragment>}
          </div>
        </details>
        <div className="hccp-card__acts">
          {canPay(r) && <button className="rpt-btn rpt-btn--green hccp-btn" onClick={() => setPayModal({ rows: [r], scope: "row" })}><Icon name="wallet" size={12} /> Pay</button>}
          <button className="rpt-btn hccp-btn hccp-btn--ghost" onClick={() => setEditModal({ row: r, mode: canEdit(r) ? "edit" : "view" })}>
            <Icon name={canEdit(r) ? "edit" : "eye"} size={12} /> {canEdit(r) ? "Edit" : "View"}
          </button>
          {canDelete(r) && <button className="rpt-btn rpt-btn--danger hccp-btn" onClick={() => setDelModal(r)}><Icon name="trash" size={12} /> Delete</button>}
        </div>
      </React.Fragment>
    );
  };

  return (
    <HrsShell
      title="Commission cashback payments"  /* label inferred — main_label `commissions_cashback_payment` resolves nowhere and renders raw live */
      subtitle="Cashback owed to players on their own GGR — generated by cron, paid from here."
      gate={<>
        Access is <code>CommissionCashbackPaymentPolicy</code>, and it is <b>Super Admin only</b>:{" "}
        <code>viewAny</code> and <code>view</code> are plain <code>isadmin()</code> (Policy L19-34), while{" "}
        <code>update</code>, <code>delete</code> and <code>pay</code> are <code>isadmin() && !paid</code>{" "}
        (L54-74) — so <b>a paid row is permanently frozen</b>: it cannot be edited, deleted or paid again.{" "}
        <code>rows()</code> additionally scopes to players whose <code>skin_id</code> is in{" "}
        <code>getSkinIDS()</code>, which for an admin is every skin.{" "}
      </>}
      gateNote={<>
        Two honest holes worth naming: <code>payment.info</code> (<code>GET …/payment/info</code>) runs with{" "}
        <b>no authorize() call at all</b> while every other action is admin-gated, and{" "}
        <code>payment.search</code> is routed to a controller method that does not exist, so it can only ever
        error. Neither is reproduced here.
      </>}
      explainer={{
        title: "What this screen is, in plain English",
        bullets: [
          <><b>This screen is reachable only by URL.</b> Its parent &ldquo;Commission cashback&rdquo; sidebar entry is
            commented out (it sits inside a <code>&lt;?/* … */?&gt;</code> block), and the Payments tab has no
            sidebar entry of its own at any time. Nothing links here from the menu — you type{" "}
            <code>/commission_cashback/payment</code> or you never see it.</>,
          <><b>Rows are produced by a cron, not by anyone here.</b>{" "}
            <code>GET /cronjobs/payCommissionCashbacks</code> queues <code>PayCommissionCashbacks</code>, which runs{" "}
            <code>CommissionCashback::pay($date)</code>: base amount = <code>SUM(players_report.profit)</code> per
            player over the period (GGR), clipped by <code>max_base_amount</code>, dropped below{" "}
            <code>min_base_amount</code>, already-paid amounts netted off. There is no &ldquo;New&rdquo; button and
            no period picker — the period is whatever the cron computed.</>,
          <><b>Paying credits the player directly.</b>{" "}
            <code>CommissionCashbackPayment::pay()</code> opens a DB transaction, locks the row, re-checks{" "}
            <code>paid</code> under the lock, then credits the player&rsquo;s <b>withdrawable</b> or{" "}
            <b>non-withdrawable</b> balance depending on the parent cashback&rsquo;s <code>balance_type</code>, and
            writes one history row (<code>transaction_type 121</code> · <code>res_type 6</code>). That lock makes it
            genuinely idempotent — unlike the platform&rsquo;s general transfer engine, which has no idempotency key.</>,
          <><b>&ldquo;Auto Pay&rdquo; in the Paid-by column is not a person.</b> A cashback with{" "}
            <code>auto_pay</code> set is settled by the cron the moment it is generated, leaving{" "}
            <code>paid_by</code> null — the column renders &ldquo;Auto Pay&rdquo; for it, a payer username for a
            manual payment, and stays blank while unpaid.</>,
          <><b>Editing an amount is an audit event with a side effect.</b> Amount + Note are required together, the
            row keeps <code>edited_by</code> and <code>edit_reason</code>, and from then on the cron <i>skips</i>{" "}
            that player/period instead of recalculating it. Edit before paying, never after — a paid row is
            read-only.</>,
          <><b>Bulk pay here asks first.</b> On the live platform &ldquo;Pay all&rdquo; and &ldquo;Pay selected&rdquo;
            execute inside the listing <code>GET …/rows</code> request through hidden carrier columns, with no
            confirmation and no CSRF — a refresh re-fires them. This rebuild keeps the same two verbs and the same
            scope rules, but makes them explicit confirmed actions.</>,
        ],
      }}
      actions={
        <button className="rpt-btn rpt-btn--search hccp-search" onClick={() => { setApplied(draft); setPage(0); setSel({}); }}>
          <Icon name="search" size={14} /> Search
        </button>
      }
    >
      <div className="hccp">
        <HccpTabs onSettings={openSettings} />

        <HrsFilters
          fields={FIELDS}
          values={draft}
          onChange={(k, v) => setDraft(d => (k === "skins" ? { ...d, skins: v, cc: "" } : { ...d, [k]: v }))}
          onSearch={(v) => { setApplied(v); setPage(0); setSel({}); }}
          onReset={resetFilters}
          resultLabel={`${hrsInt(filtered.length)} of ${hrsInt(rows.length)}`}
        />

        <HccpBulkBar
          pageUnpaid={pageUnpaid}
          totalUnpaid={totalUnpaid}
          selectedCount={selectedRows.length}
          allPageSelected={allPageSelected}
          onSelectAll={selectAllPage}
          onPaySelected={() => setPayModal({ rows: selectedRows, scope: "selected" })}
          onPayAll={() => setPayModal({ rows: filtered.filter(r => !r.paid), scope: "all" })}
        />

        <HrsTable
          columns={COLUMNS}
          rows={view}
          sort={sort}
          onSort={(next) => { setSort(next); setPage(0); }}
          rowKey="id"
          renderCard={renderCard}
          empty="No data available in table" /* DataTables' own empty string */
        />

        <HrsPager
          page={page}
          pageSize={pageSize}
          total={filtered.length}
          onPage={(p) => { setPage(p); setSel({}); }}
          onPageSize={(s) => { setPageSize(s); setPage(0); setSel({}); }}
          sizes={HCCP_PAGE_SIZES}
        />

        {/* No HrsExport: no export button is configured on the real screen. */}
        {/* No HrsKpis: the controller computes no totals — see the bulk bar note. */}
        <HccpAbsences />
      </div>

      <HccpPayModal payload={payModal} onClose={() => setPayModal(null)} onConfirm={runPay} />
      {editModal && <HccpEditModal key={`e${editModal.row.id}${editModal.mode}`} row={editModal.row} mode={editModal.mode}
        onClose={() => setEditModal(null)} onSave={runSave} />}
      <HccpDeleteModal row={delModal} onClose={() => setDelModal(null)} onConfirm={runDelete} />
    </HrsShell>
  );
};
