// Represents: Admin/CommissionCashbackController — see docs/ISYSTEM_REFERENCE.md §Batch 4 "Acca bonus payments + Commission cashback"
/* ====================================================================
   COMMISSION CASHBACK — Settings surface (Batch 4 gap-fill), Hrs* shell
   ====================================================================
   Real screen: `GET /commission_cashback` (`admin.commission_cashback.index`,
   routes/admin.php L246, inside the `commission_cashback.` group L232 which
   itself sits in the `admin.` auth group L25 — so NO `/admin` URI prefix) →
   Admin/CommissionCashbackController::index L18 · rows L46 (DataTables JSON) ·
   create L154 · store L169 · edit L245 · delete L262 · search L277.
   Views: the shared generic table shell `admin.generics.index` + filter
   partial generics/filters/commission_cashback.blade.php (which also renders
   the Settings/Payments tab bar, L1-23) + create/edit modal body
   generics/models/commission_cashback.blade.php, driven by
   public/js/pages/generic/commission_cashback.js.

   This file is the CONFIGURATION half only. The sibling "Payments" tab
   (`admin.commission_cashback.payment.index` → GET /commission_cashback/payment,
   Admin/CommissionCashbackPaymentController) is a separate screen and lives in
   its own file — the tab bar below links to it, it is not rebuilt here.

   WHAT A COMMISSION CASHBACK ACTUALLY IS
   - A rule row in `commission_cashbacks` that pays players back a percentage
     of the house's GGR on them, per period, per skin, per game category.
   - It generates nothing by itself. The cron `GET /cronjobs/payCommissionCashbacks`
     (routes/cronjobs.php L119 → CronsController::payCommissionCashbacks L181 →
     queued App\Jobs\PayCommissionCashbacks → CommissionCashback::pay($date),
     app/Models/CommissionCashback.php L115-358) walks the active rules and
     writes `commission_cashback_payment` rows.
   - Base amount = SUM(`players_report.profit`) per player over the period
     (i.e. GGR), floored by `min_base_amount`, capped by `max_base_amount`.
     Periods that already carry an admin-EDITED payment are skipped; amounts
     already paid are netted off. `auto_pay` makes the cron immediately call
     CommissionCashbackPayment::pay() instead of leaving the row To pay.
   - Membership lives in three pivots — `commission_cashback_skins`,
     `commission_cashback_game_categories`, `commission_cashback_period_days`
     (migrations 2026_06_24_155202_*). `period_days` is synced on save via
     CommissionCashbackPeriodDay::updateOrCreate plus a delete of removed days
     (store L230-240).

   Faithful absences (nothing added — brief §3):
   - NO KPIs / totals. The only aggregate anywhere near this screen is the
     modal's "next payments" preview, which is a date list, not a figure.
   - NO export. The generic layout loads the DataTables Buttons/pdfmake CDN
     bundles, but commission_cashback.js configures no buttons.
     // <!-- SUGGESTION: stop loading the export CDN bundles on screens that
     //      configure no export buttons — today they ship unused. -->
   - NO bulk actions, no row checkboxes (those exist only on the Payments tab).
   - NO date / active / auto_pay filters: the filter scaffolding for them is
     commented out in rows() L73-96, so only ID and Skins are live.
   - NO status/lifecycle actions beyond Edit + Delete. There is no "run now",
     no preview-payments-for-this-rule action; only the cron creates payments.
   - NO per-row policy flags: commission_cashback.js decides which actions to
     render from a client-side `user.is_admin`, not from server-sent booleans.

   Divergences implemented as evident intent (known-bug policy, CLAUDE.md):
   1. Sorting — the generic DataTable marks every column except Actions
      orderable, but rows() has its ENTIRE order-by mapping commented out
      (L98-112) and always returns `commission_cashbacks.id DESC` (L55). On the
      live platform clicking a header re-requests and gets the same order back.
      Here the data columns sort for real, with id DESC as the default so the
      first paint matches the server.
      // <!-- SUGGESTION: uncomment/repair the order-by mapping in
      //      CommissionCashbackController::rows, or mark the headers
      //      non-orderable so they stop advertising sorting that never happens. -->
   2. Edit-mode id validation — store() sets `$rules['id'] = 'exists:mobile_validators'`
      on the update path (L192), validating a commission-cashback id against the
      `mobile_validators` table (copy-paste from another generic screen). Saving
      an edit therefore succeeds or fails based on an unrelated table's ids. The
      evident intent — the id must be an existing commission cashback — is what
      this rebuild enforces.
      // <!-- SUGGESTION: change store() L192 to `exists:commission_cashbacks,id`. -->

   Real-platform findings surfaced on screen (not fixed, not hidden):
   - The screen is ORPHANED NAVIGATION. Its sidebar entry (sidebar.blade.php
     L905-912, guarded `@if( isadmin() )`) sits inside a PHP block-comment
     wrapper opened at L890 and closed at L913, so it never renders for anyone. The
     page is reachable only by typing `/commission_cashback`. Surfaced in a
     dedicated Explainer below, per the build brief.
   - `store()` authorizes the `create` ability even when it is updating an
     existing row (L171). Harmless today because CommissionCashbackPolicy makes
     create and update the same isadmin() check — but it means a future
     read-mostly admin variant would silently get write access on edit.
   - `admin.commission_cashback.show → GET /commission_cashback/{id}` (L252) is
     routed to a `show()` method the controller does not have; hitting it errors.
     The non-admin branch of commission_cashback.js links row names there.
   - rows() L59-60 has a dead non-admin branch that filters
     `commission_cashback_skins.skin_id` without joining that pivot — it would
     raise a SQL error, but CommissionCashbackPolicy::viewAny already blocks
     every non-admin, so it is unreachable.
   - `days[]` has NO validation rules at all: whatever the client posts is
     written into `commission_cashback_period_days` (store L230-240). Weekday
     numbers outside 1-7, or month days outside 1-31, are accepted.
   - The modal's "next payments" preview is fed by
     `GET admin.commission_cashback.payment.info` (admin.php L237, controller
     L335-430) — a route with NO authorize() call at all. Any authenticated,
     2FA'd back-office user can read cashback schedule data from it.
   - `apply_over` is never a form field: store() hardcodes `'ggr'` (L216). The
     schema's other enum value, `turnover`, hits an unimplemented TODO branch in
     CommissionCashback::pay() (Model L267-272), so it must not be selectable.

   Label policy (CLAUDE.md): `backend.commission_cashback`,
   `backend.new_commission_cashback`, `backend.percentage_to_be_paid`,
   `backend.repeat_it_every`, `backend.periodicity`, `backend.days`,
   `backend.every`, `backend.balance_type`, `backend.payments` and
   `backend.daily` resolve in no committed lang file (storage/lang is
   gitignored) — operator-facing wording is written here and marked
   "label inferred". Resolved keys used verbatim: settings "Settings",
   actions "Actions", search_button "Search", yes/no "Yes"/"No",
   extras.auto_pay "Auto Pay", extras.weekly "Weekly", extras.monthly
   "Monthly", balance_withdrawable "Withdrawable balance", balance
   "Non withdrawable balance".

   Demo session = Super admin (user_level 0) — the only role that passes
   CommissionCashbackPolicy, so every action renders.

   All new top-level names are hccb/Hccb/HCCB-prefixed except the required page
   component `CommissionCashback`.
   ==================================================================== */

const { useState: hccbUseState, useMemo: hccbUseMemo } = React;

const hccbToast = (title, detail) => window.PAYBO && window.PAYBO.emitToast && window.PAYBO.emitToast({
  id: `hccb-${Date.now()}-${Math.floor(Math.random() * 1e6)}`,
  tx_id: title, amount: 0, currency: "HOST", player: "Commission cashback", reason: detail,
});

/* Sibling screen — the "Payments" half of the tab bar. The real platform's pair
   is /commission_cashback (this file) and /commission_cashback/payment; in this
   prototype they are route keys `comm-cashback` (/commissions/cashback) and
   `comm-cashback-payments` (/commissions/cashback-payments), per src/routes.jsx.
   Keep this in step with that table if the payments path is ever renamed. */
const HCCB_PAYMENTS_PATH = "/commissions/cashback-payments";

/* ---------- deterministic PRNG (mulberry32) — rows render identically on
     every load, like the rest of the prototype's mock data ---------- */
const hccbRand = (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 hccbPad = (n) => String(n).padStart(2, "0");
/* Stored as DATE; every display in the real screen is risistemadata() = d/m/Y. */
const hccbIso = (d) => `${d.getFullYear()}-${hccbPad(d.getMonth() + 1)}-${hccbPad(d.getDate())}`;
const hccbDmy = (d) => `${hccbPad(d.getDate())}/${hccbPad(d.getMonth() + 1)}/${d.getFullYear()}`;
/* iso "YYYY-MM-DD" → "DD/MM/YYYY" (the datepicker format store() expects). */
const hccbDmyIso = (iso) => {
  if (!iso) return "";
  const p = String(iso).split("-");
  return p.length === 3 ? `${p[2]}/${p[1]}/${p[0]}` : String(iso);
};
const hccbNum = (n) => (n == null || n === "" ? "" : Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }));

const HCCB_PAGE_SIZES = [5, 10, 25, 50];   // ajax.js lengthMenu; controller default length 50 (L53)

/* Skins come from SkinsController::getSkinsList() in the form and from
   Skin::orderBy('name') in the filter. Same catalogue the rest of the
   prototype's Host pages use. */
const HCCB_SKINS = [
  { id: 47, name: "win24hs" },
  { id: 52, name: "apostando365" },
  { id: 55, name: "apuestadepana" },
  { id: 58, name: "PlaySpin" },
  { id: 60, name: "Anchodeespada" },
  { id: 62, name: "Jokerenvivo" },
  { id: 64, name: "Donjoker" },
  { id: 66, name: "Juegojoker" },
  { id: 68, name: "Tucasino" },
  { id: 70, name: "Jugaygana" },
];

/* The form offers exactly three categories and validation is `in:1,2,4` —
   Sport (6) and Poker (5) exist in GameCategory but are NOT selectable here. */
const HCCB_GAME_CATS = [
  { id: 1, name: "Casino" },
  { id: 2, name: "Casino Live" },
  { id: 4, name: "Virtual" },
];

const HCCB_PERIODICITY = [
  { value: "daily", label: "Daily" },     /* label inferred — backend.daily is absent from the committed en lang */
  { value: "weekly", label: "Weekly" },   /* backend.extras.weekly */
  { value: "monthly", label: "Monthly" }, /* backend.extras.monthly */
];

const HCCB_BALANCE_TYPES = [
  { value: "balance_withdrawable", label: "Withdrawable balance" },     /* backend.php:209-210 */
  { value: "balance", label: "Non withdrawable balance" },
];

/* getDaysLabel() prints translated `backend.day_1` … `backend.day_7`.
   UNCLEAR: the reference does not state whether day_1 is Monday or Sunday —
   ISO (1 = Monday) is assumed here and the assumption is shown in the form. */
const HCCB_DAY_NAMES = [
  { value: 1, label: "Monday" }, { value: 2, label: "Tuesday" }, { value: 3, label: "Wednesday" },
  { value: 4, label: "Thursday" }, { value: 5, label: "Friday" }, { value: 6, label: "Saturday" },
  { value: 7, label: "Sunday" },
];
const HCCB_MONTH_DAYS = Array.from({ length: 31 }, (_, i) => i + 1);

const hccbSkinName = (id) => (HCCB_SKINS.find(s => s.id === Number(id)) || { name: `Skin #${id}` }).name;
const hccbCatName = (id) => (HCCB_GAME_CATS.find(c => c.id === Number(id)) || { name: `Category #${id}` }).name;
const hccbPeriodicityLabel = (v) => (HCCB_PERIODICITY.find(p => p.value === v) || { label: "Unknown" }).label; /* backend.unknown fallback */
const hccbBalanceLabel = (v) => (HCCB_BALANCE_TYPES.find(b => b.value === v) || { label: "Unknown" }).label;

/* getDaysLabel(): weekly → weekday names · monthly → day numbers ·
   daily → blank · nothing selected → "All". */
const hccbDaysLabel = (row) => {
  if (row.periodicity === "daily") return "";
  const days = (row.days || []).slice().sort((a, b) => a - b);
  if (!days.length) return "All";
  if (row.periodicity === "weekly") return days.map(d => (HCCB_DAY_NAMES.find(n => n.value === d) || { label: d }).label).join(", ");
  return days.join(", ");
};

/* ------------------------------------------------------------------ *
 * "Next payments" preview — the create/edit modal renders a live list of
 * upcoming payment dates fed by GET admin.commission_cashback.payment.info
 * (blade L245-283 → CommissionCashbackPaymentController::info L335-430).
 *
 * UNCLEAR: the reference documents that endpoint only as "next-payment-dates
 * preview JSON" — its exact stepping rules, how many dates it returns and
 * whether it clamps to `end` are not specified. This client-side stand-in
 * steps from `start` by `every` periods, keeps the selected days, stops at
 * `end`, and shows at most 6 dates. Treat the dates as illustrative.
 * ------------------------------------------------------------------ */
const hccbNextPayments = (cfg, limit = 6) => {
  const out = [];
  if (!cfg.start || !cfg.end || !cfg.periodicity) return out;
  const start = new Date(`${cfg.start}T00:00:00`);
  const end = new Date(`${cfg.end}T00:00:00`);
  if (isNaN(start.getTime()) || isNaN(end.getTime()) || end < start) return out;
  const every = Math.max(1, Number(cfg.every) || 1);
  const days = (cfg.days || []).map(Number).filter(n => !isNaN(n));

  if (cfg.periodicity === "daily") {
    for (let d = new Date(start), i = 0; d <= end && out.length < limit && i < 4000; i++, d.setDate(d.getDate() + every)) {
      out.push(new Date(d));
    }
    return out;
  }
  if (cfg.periodicity === "weekly") {
    const want = days.length ? days : HCCB_DAY_NAMES.map(d => d.value);   // none selected = "All"
    for (let d = new Date(start), i = 0; d <= end && out.length < limit && i < 4000; i++, d.setDate(d.getDate() + 1)) {
      const iso = d.getDay() === 0 ? 7 : d.getDay();                      // ISO weekday, per the day_1..7 assumption
      if (Math.floor(i / 7) % every === 0 && want.indexOf(iso) !== -1) out.push(new Date(d));
    }
    return out;
  }
  const want = (days.length ? days : HCCB_MONTH_DAYS).slice().sort((a, b) => a - b);
  let cursor = new Date(start.getFullYear(), start.getMonth(), 1);
  for (let m = 0; cursor <= end && out.length < limit && m < 400; m++) {
    if (m % every === 0) {
      for (const dd of want) {
        const cand = new Date(cursor.getFullYear(), cursor.getMonth(), dd);
        if (cand.getMonth() !== cursor.getMonth()) continue;              // e.g. day 31 in a 30-day month
        if (cand >= start && cand <= end && out.length < limit) out.push(cand);
      }
    }
    cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1);
  }
  return out;
};

/* ------------------------------------------------------------------ *
 * Seed rows — `commission_cashbacks` + its three pivots. Shapes mirror the
 * migration (2026_06_24_155202_create_commission_cashbacks_table.php):
 * name, cashback_percentage, min/max_base_amount, start, end, periodicity,
 * every, balance_type, apply_over (always 'ggr'), active, auto_pay.
 * ------------------------------------------------------------------ */
const HCCB_SEED_ROWS = (() => {
  const rnd = hccbRand(40714);
  const pick = (arr) => arr[Math.floor(rnd() * arr.length)];
  const names = [
    "Casino weekly GGR cashback", "Live tables monthly rebate", "Virtual daily giveback",
    "VIP casino cashback", "Weekend live cashback", "Month-end loyalty rebate",
    "Low roller daily cashback", "High roller monthly cashback", "Casino + Live combo rebate",
    "Retention push cashback", "Slots-only weekly rebate", "Quarterly VIP giveback",
    "New skin launch cashback", "Winback cashback",
  ];
  return names.map((name, i) => {
    const periodicity = pick(["daily", "weekly", "monthly"]);
    const skinCount = 1 + Math.floor(rnd() * 3);
    const skins = [];
    while (skins.length < skinCount) {
      const s = HCCB_SKINS[Math.floor(rnd() * HCCB_SKINS.length)].id;
      if (skins.indexOf(s) === -1) skins.push(s);
    }
    const cats = HCCB_GAME_CATS.filter(() => rnd() > 0.42).map(c => c.id);
    if (!cats.length) cats.push(1);
    let days = [];
    if (periodicity === "weekly") days = HCCB_DAY_NAMES.filter(() => rnd() > 0.6).map(d => d.value);
    if (periodicity === "monthly") days = HCCB_MONTH_DAYS.filter(() => rnd() > 0.9).map(d => d);
    const startD = new Date(2026, Math.floor(rnd() * 6), 1 + Math.floor(rnd() * 20));
    const endD = new Date(startD.getFullYear(), startD.getMonth() + 3 + Math.floor(rnd() * 6), startD.getDate());
    const hasMin = rnd() > 0.45;
    const hasMax = rnd() > 0.55;
    const min = hasMin ? Math.round(rnd() * 40) * 250 + 500 : null;
    return {
      id: 100 + i,
      name,
      skins,
      gameCats: cats.sort((a, b) => a - b),
      start: hccbIso(startD),
      end: hccbIso(endD),
      periodicity,
      days: days.sort((a, b) => a - b),
      every: 1 + Math.floor(rnd() * 6),                       // validation caps at 6 (lte:6)
      balanceType: rnd() > 0.4 ? "balance_withdrawable" : "balance",
      cashbackPercentage: Math.round((0.5 + rnd() * 9.5) * 10) / 10,
      minBase: min,
      maxBase: hasMax ? (min || 0) + Math.round(rnd() * 60) * 500 + 5000 : null,
      applyOver: "ggr",                                       // store() L216 forces this; not a form field
      active: rnd() > 0.25,
      autoPay: rnd() > 0.55,
    };
  }).reverse();                                               // listing is id DESC (rows L55)
})();

/* ------------------------------------------------------------------ *
 * Settings / Payments tab bar — rendered above BOTH commission-cashback
 * screens by their filter partials (filters/commission_cashback.blade.php
 * L13-18 and filters/commissions_cashback_payment.blade.php L13-18).
 * ------------------------------------------------------------------ */
const HccbTabs = () => (
  <div className="hccb-tabs" role="tablist" aria-label="Commission cashback">
    <span className="hccb-tab hccb-tab--on" role="tab" aria-selected="true">Settings</span>{/* backend.settings */}
    <a className="hccb-tab" role="tab" aria-selected="false" href={HCCB_PAYMENTS_PATH}>
      Payments{/* label inferred */}
      <Icon name="chevron_right" size={12} />
    </a>
  </div>
);

/* Modal chrome — shared .bp-modal, full-screen on mobile (brief §11). The real
   modal is the generic shell's `genericModal` (index.blade.php L130-147) with
   the body from generics/models/commission_cashback.blade.php. */
const HccbModal = ({ title, sub, onClose, children, footer, wide }) => (
  <div className="bp-modal-scrim hccb-scrim" onClick={onClose}>
    <div className={`bp-modal hccb-modal${wide ? " hccb-modal--wide" : ""}`} onClick={e => e.stopPropagation()}>
      <div className="hccb-modal__head">
        <div>
          <div className="hccb-modal__title">{title}</div>
          {sub && <div className="hccb-modal__sub">{sub}</div>}
        </div>
        <button className="hccb-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hccb-modal__body">{children}</div>
      {footer && <div className="hccb-modal__foot">{footer}</div>}
    </div>
  </div>
);

const HccbYesNo = ({ on }) => (
  <span className={`hccb-yn${on ? " hccb-yn--on" : ""}`}>{on ? "Yes" : "No"}</span>
);

const HccbChips = ({ items, title }) => (
  <span className="hccb-chips" title={title}>
    {items.length === 0 && <span className="hccb-chip hccb-chip--none">—</span>}
    {items.map((t, i) => <span className="hccb-chip" key={i}>{t}</span>)}
  </span>
);

const HccbFinding = ({ tone = "warn", title, children }) => (
  <div className={`hccb-finding hccb-finding--${tone}`}>
    <div className="hccb-finding__ic"><Icon name={tone === "err" ? "alert" : "info"} size={13} /></div>
    <div>
      <div className="hccb-finding__t">{title}</div>
      <div className="hccb-finding__b">{children}</div>
    </div>
  </div>
);

/* ------------------------------------------------------------------ *
 * Create / Edit form — GET /commission_cashback/create (new) or
 * /commission_cashback/{id}/edit, then POST /commission_cashback/{id?}.
 * There is no FormRequest: store() builds the rules inline (L173-189) and
 * answers ajaxError($msg, ["campierrati" => [...]]) — one message plus the
 * list of offending field names, which the shared modal turns into red field
 * outlines. Array fields arrive in that list suffixed `[]` (L203).
 * ------------------------------------------------------------------ */
const HccbFormModal = ({ row, onClose, onSave }) => {
  const isNew = !row;
  const [name, setName] = hccbUseState(row ? row.name : "");
  const [skins, setSkins] = hccbUseState(row ? row.skins.slice() : []);
  const [cats, setCats] = hccbUseState(row ? row.gameCats.slice() : []);
  const [pct, setPct] = hccbUseState(row ? String(row.cashbackPercentage) : "");
  const [minBase, setMinBase] = hccbUseState(row && row.minBase != null ? String(row.minBase) : "");
  const [maxBase, setMaxBase] = hccbUseState(row && row.maxBase != null ? String(row.maxBase) : "");
  const [start, setStart] = hccbUseState(row ? row.start : "");
  const [end, setEnd] = hccbUseState(row ? row.end : "");
  const [periodicity, setPeriodicity] = hccbUseState(row ? row.periodicity : "");
  const [every, setEvery] = hccbUseState(row ? String(row.every) : "1");
  const [days, setDays] = hccbUseState(row ? row.days.slice() : []);
  const [balanceType, setBalanceType] = hccbUseState(row ? row.balanceType : "balance_withdrawable");
  const [active, setActive] = hccbUseState(row ? row.active : true);
  const [autoPay, setAutoPay] = hccbUseState(row ? row.autoPay : false);
  const [errs, setErrs] = hccbUseState({});
  const [banner, setBanner] = hccbUseState("");

  /* The blade disables Periodicity and the day selectors on CREATE until both
     dates are chosen (the JS re-enables them once start+end are set). Editing
     an existing row always has dates, so they stay enabled there. */
  const datesReady = !!(start && end);
  const lockPeriod = isNew && !datesReady;

  const toggleIn = (list, setList, v) => setList(list.indexOf(v) === -1 ? [...list, v] : list.filter(x => x !== v));

  const preview = hccbUseMemo(
    () => hccbNextPayments({ start, end, periodicity, every, days }),
    [start, end, periodicity, every, days]
  );

  const save = () => {
    /* Mirrors store()'s inline Validator::make (L173-189). */
    const e = {};
    if (!name.trim()) e.name = "Name is required.";
    else if (name.trim().length > 255) e.name = "Name must not be longer than 255 characters.";
    if (!skins.length) e.skins = "Pick at least one skin.";
    if (!cats.length) e.cats = "Pick at least one game category.";
    if (pct === "" || isNaN(Number(pct))) e.pct = "Percentage to be paid is required and must be a number.";
    else if (Number(pct) <= 0) e.pct = "Percentage to be paid must be greater than 0.";
    if (minBase !== "" && isNaN(Number(minBase))) e.minBase = "Min base amount must be a number.";
    if (maxBase !== "" && isNaN(Number(maxBase))) e.maxBase = "Max base amount must be a number.";
    else if (maxBase !== "" && minBase !== "" && Number(maxBase) <= Number(minBase)) e.maxBase = "Max base amount must be greater than min base amount.";
    if (!start) e.start = "UTC Start date is required.";
    if (!end) e.end = "UTC End date is required.";
    else if (start && end && new Date(end) <= new Date(start)) e.end = "UTC End date must be after the start date.";
    if (!periodicity) e.periodicity = "Periodicity is required.";
    if (every === "" || isNaN(Number(every))) e.every = "Repeat it every is required.";
    else if (Number(every) <= 0) e.every = "Repeat it every must be greater than 0.";
    else if (Number(every) > 6) e.every = "Repeat it every must be 6 or lower.";   // lte:6
    if (!balanceType) e.balanceType = "Balance type is required.";
    /* KNOWN BUG — DIVERGENCE (see header): on edit the real store() adds
       `$rules['id'] = 'exists:mobile_validators'`, checking this row's id
       against an unrelated table instead of `commission_cashbacks`. The evident
       intent — the id must be an existing commission cashback — is what applies
       here: the modal is only ever opened from a live row, so the id is valid by
       construction and no bogus mobile_validators lookup can reject the save. */

    setErrs(e);
    if (Object.keys(e).length) {
      /* One ajaxError message + the campierrati field list — array fields come
         back as `skins[]`, `game_categories[]`. */
      setBanner("Some fields are not valid. Check the highlighted inputs.");
      return;
    }
    setBanner("");
    onSave({
      id: isNew ? null : row.id,
      name: name.trim(),
      skins: skins.slice(),
      gameCats: cats.slice(),
      cashbackPercentage: Number(pct),
      minBase: minBase === "" ? null : Number(minBase),      // '' coerced to null (store L212)
      maxBase: maxBase === "" ? null : Number(maxBase),
      start, end, periodicity,
      every: Number(every),
      days: days.slice(),
      balanceType,
      applyOver: "ggr",                                      // forced server-side (store L216)
      active, autoPay,
    });
    onClose();
  };

  const err = (k) => errs[k] ? <div className="hccb-fielderr">{errs[k]}</div> : null;
  const cls = (k, base) => `${base}${errs[k] ? " hccb-invalid" : ""}`;

  return (
    <HccbModal
      wide
      /* label inferred — the generic shell's create button is
         __('backend.new_commission_cashback'), a key absent from the committed lang. */
      title={isNew ? "New Commission Cashback" : `Edit Commission Cashback #${row.id}`}
      sub={isNew ? "POST /commission_cashback — created_by is stamped server-side" : "POST /commission_cashback/{id} — edited_by is stamped server-side"}
      onClose={onClose}
      footer={
        <>
          <button className="btn btn--secondary" onClick={onClose}>Cancel</button>
          <button className="btn btn--primary" onClick={save}><Icon name="check" size={13} /> Save</button>
        </>
      }>

      {banner && <div className="hccb-banner"><Icon name="alert" size={13} /> {banner}</div>}

      <div className="hccb-sectitle">Rule</div>
      <div className="hccb-grid">
        <div className="hccb-field hccb-field--full">
          <label className="hccb-label">Name <span className="hccb-req">*</span></label>
          <input className={cls("name", "input")} value={name} maxLength={255}
            placeholder="e.g. Casino weekly GGR cashback" onChange={e => setName(e.target.value)} />
          {err("name")}
        </div>

        <div className="hccb-field">
          <label className="hccb-label">
            Skins <span className="hccb-req">*</span>
            <Tip>Multi-select of <code>SkinsController::getSkinsList()</code>. Each id is validated with <code>exists:skins,id</code> and written to the <code>commission_cashback_skins</code> pivot.</Tip>
          </label>
          <div className={cls("skins", "hccb-picker")}>
            {HCCB_SKINS.map(s => (
              <button type="button" key={s.id}
                className={`hccb-opt${skins.indexOf(s.id) !== -1 ? " hccb-opt--on" : ""}`}
                onClick={() => toggleIn(skins, setSkins, s.id)}>{s.name}</button>
            ))}
          </div>
          {err("skins")}
        </div>

        <div className="hccb-field">
          <label className="hccb-label">
            Game categories <span className="hccb-req">*</span>
            <Tip>Validated with <code>in:1,2,4</code> — only Casino, Casino Live and Virtual can be selected here, even though other game categories exist on the platform. Stored in <code>commission_cashback_game_categories</code>.</Tip>
          </label>
          <div className={cls("cats", "hccb-picker")}>
            {HCCB_GAME_CATS.map(c => (
              <button type="button" key={c.id}
                className={`hccb-opt${cats.indexOf(c.id) !== -1 ? " hccb-opt--on" : ""}`}
                onClick={() => toggleIn(cats, setCats, c.id)}>{c.name} <span className="hccb-optid">{c.id}</span></button>
            ))}
          </div>
          {err("cats")}
        </div>
      </div>

      <div className="hccb-sectitle">Amounts</div>
      <div className="hccb-grid">
        <div className="hccb-field">
          {/* backend.percentage_to_be_paid — label inferred */}
          <label className="hccb-label">
            Percentage to be paid <span className="hccb-req">*</span>
            <Tip>Applied to the player's base amount for the period. The base amount is <code>SUM(players_report.profit)</code> — the house's GGR on that player.</Tip>
          </label>
          <div className="hccb-suffixed">
            <input className={cls("pct", "input")} inputMode="decimal" value={pct} placeholder="0.00" onChange={e => setPct(e.target.value)} />
            <span className="hccb-suffix">%</span>
          </div>
          {err("pct")}
        </div>

        <div className="hccb-field">
          <label className="hccb-label">
            Min base amount
            <Tip>Threshold: a player whose base amount for the period is below this gets no payment. Optional — an empty value is coerced to <code>null</code>.</Tip>
          </label>
          <input className={cls("minBase", "input")} inputMode="decimal" value={minBase} placeholder="Optional" onChange={e => setMinBase(e.target.value)} />
          {err("minBase")}
        </div>

        <div className="hccb-field">
          <label className="hccb-label">
            Max base amount
            <Tip>Cap: base amounts above this are clamped down to it before the percentage is applied. Must be greater than the min base amount when both are set.</Tip>
          </label>
          <input className={cls("maxBase", "input")} inputMode="decimal" value={maxBase} placeholder="Optional" onChange={e => setMaxBase(e.target.value)} />
          {err("maxBase")}
        </div>
      </div>
      {/* UNCLEAR: the reference does not state the currency or scale of
          min_base_amount / max_base_amount. They are stored as plain numerics
          and the payments table keeps its own min/max copies per payment row. */}
      <div className="hccb-hint">
        <b>Applied over:</b> GGR — fixed. <code>apply_over</code> is never posted by this form; <code>store()</code> hardcodes <code>'ggr'</code>.
        The schema's other value, <code>turnover</code>, reaches an unimplemented branch in <code>CommissionCashback::pay()</code>, so it is deliberately not offered.
      </div>

      <div className="hccb-sectitle">Schedule</div>
      <div className="hccb-grid">
        <div className="hccb-field">
          <label className="hccb-label">
            UTC Start date <span className="hccb-req">*</span>
            <Tip>Posted as <code>d/m/Y</code> and converted by <code>sistemadata()</code> before saving.</Tip>
          </label>
          <input type="date" className={cls("start", "input")} value={start} onChange={e => setStart(e.target.value)} />
          <div className="hccb-hint">Posts as <code>{start ? hccbDmyIso(start) : "dd/mm/yyyy"}</code></div>
          {err("start")}
        </div>
        <div className="hccb-field">
          <label className="hccb-label">UTC End date <span className="hccb-req">*</span></label>
          <input type="date" className={cls("end", "input")} value={end} onChange={e => setEnd(e.target.value)} />
          <div className="hccb-hint">Posts as <code>{end ? hccbDmyIso(end) : "dd/mm/yyyy"}</code></div>
          {err("end")}
        </div>
        <div className="hccb-field">
          <label className="hccb-label">
            Periodicity <span className="hccb-req">*</span>
            <Tip>Decides which day selector applies below. Disabled until both dates are set when creating a new rule — that is the real form's own behaviour, not an addition.</Tip>
          </label>
          <select className={cls("periodicity", "select")} value={periodicity} disabled={lockPeriod}
            onChange={e => { setPeriodicity(e.target.value); setDays([]); }}>
            <option value="">- Select -</option>
            {HCCB_PERIODICITY.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
          </select>
          {err("periodicity")}
        </div>
        <div className="hccb-field">
          {/* backend.repeat_it_every — label inferred */}
          <label className="hccb-label">
            Repeat it every <span className="hccb-req">*</span>
            <Tip>Number of periods between payments. The input itself only sets <code>min=1</code>; the server additionally rejects anything above 6 (<code>lte:6</code>).</Tip>
          </label>
          <div className="hccb-suffixed">
            <input type="number" min="1" className={cls("every", "input")} value={every} onChange={e => setEvery(e.target.value)} />
            <span className="hccb-suffix">{periodicity === "weekly" ? "week(s)" : periodicity === "monthly" ? "month(s)" : "day(s)"}</span>
          </div>
          {err("every")}
        </div>
        <div className="hccb-field">
          <label className="hccb-label">
            Balance type <span className="hccb-req">*</span>
            <Tip>Which wallet <code>CommissionCashbackPayment::pay()</code> credits — <code>balance_withdrawable</code> or <code>balance</code>.</Tip>
          </label>
          <select className={cls("balanceType", "select")} value={balanceType} onChange={e => setBalanceType(e.target.value)}>
            {HCCB_BALANCE_TYPES.map(b => <option key={b.value} value={b.value}>{b.label}</option>)}
          </select>
          {err("balanceType")}
        </div>
      </div>

      <div className="hccb-field">
        <label className="hccb-label">
          Days
          <Tip>Written straight into <code>commission_cashback_period_days</code>. This field has <b>no validation rules at all</b> on the real platform — whatever is posted is stored.</Tip>
        </label>
        {lockPeriod && <div className="hccb-hint">Choose a start and end date first — the real form keeps this selector disabled until then.</div>}
        {!lockPeriod && periodicity === "" && <div className="hccb-hint">Pick a periodicity to choose days.</div>}
        {!lockPeriod && periodicity === "daily" && (
          <div className="hccb-hint">Daily rules use no day selector, and the list column renders blank for them.</div>
        )}
        {!lockPeriod && periodicity === "weekly" && (
          <>
            <div className="hccb-picker">
              {HCCB_DAY_NAMES.map(d => (
                <button type="button" key={d.value}
                  className={`hccb-opt${days.indexOf(d.value) !== -1 ? " hccb-opt--on" : ""}`}
                  onClick={() => toggleIn(days, setDays, d.value)}>{d.label} <span className="hccb-optid">{d.value}</span></button>
              ))}
            </div>
            {/* UNCLEAR: backend.day_1..day_7 are the translated weekday names, but the
                reference does not say whether day_1 is Monday or Sunday. ISO assumed. */}
            <div className="hccb-hint">Numbers are the stored <code>day</code> values (<code>backend.day_1</code>…<code>day_7</code>). Which weekday <code>day_1</code> maps to is not documented — Monday is assumed here.</div>
          </>
        )}
        {!lockPeriod && periodicity === "monthly" && (
          <>
            <div className="hccb-picker hccb-picker--dense">
              {HCCB_MONTH_DAYS.map(d => (
                <button type="button" key={d}
                  className={`hccb-opt hccb-opt--num${days.indexOf(d) !== -1 ? " hccb-opt--on" : ""}`}
                  onClick={() => toggleIn(days, setDays, d)}>{d}</button>
              ))}
            </div>
            <div className="hccb-hint">Days 29-31 are simply skipped in months that do not have them.</div>
          </>
        )}
        {!lockPeriod && periodicity !== "" && periodicity !== "daily" && days.length === 0 && (
          <div className="hccb-hint">Nothing selected — the list column shows <b>All</b> for this rule.</div>
        )}
      </div>

      <div className="hccb-sectitle">Status</div>
      <div className="hccb-switches">
        <label className="hccb-switch">
          <Toggle value={active} onChange={setActive} onLabel="" offLabel="" size="sm" />
          <span>
            <b>Active</b>
            <em>Only active rules are picked up by the <code>payCommissionCashbacks</code> cron.</em>
          </span>
        </label>
        <label className="hccb-switch">
          {/* backend.extras.auto_pay = "Auto Pay" */}
          <Toggle value={autoPay} onChange={setAutoPay} onLabel="" offLabel="" size="sm" />
          <span>
            <b>Auto Pay</b>
            <em>Generated payments are paid immediately by the cron instead of waiting for an operator on the Payments tab.</em>
          </span>
        </label>
      </div>

      <div className="hccb-sectitle">Next payments</div>
      <div className="hccb-preview">
        <div className="hccb-preview__head">
          <span>Preview of upcoming payment dates</span>
          <span className="hccb-preview__src">GET /commission_cashback/payment/info</span>
        </div>
        {preview.length === 0
          ? <div className="hccb-preview__empty">Set a start date, an end date and a periodicity to preview the schedule.</div>
          : (
            <div className="hccb-preview__list">
              {preview.map((d, i) => <span className="hccb-preview__d" key={i}>{hccbDmy(d)}</span>)}
            </div>
          )}
        <div className="hccb-hint">
          Illustrative only. The live preview comes from an endpoint that carries <b>no authorization check</b> — any authenticated back-office user can call it.
        </div>
        {/* <!-- SUGGESTION: add an authorize('viewAny', CommissionCashback::class) call to
             CommissionCashbackPaymentController::info — it is the only route in the
             commission_cashback group with no gate at all. --> */}
      </div>
    </HccbModal>
  );
};

/* Delete — deleteConfirm modal → DELETE /commission_cashback/{id} (L253),
   policy `delete` = isadmin(). */
const HccbDeleteDialog = ({ row, onClose, onConfirm }) => (
  <HccbModal
    title="Delete commission cashback"
    sub={`DELETE /commission_cashback/${row.id}`}
    onClose={onClose}
    footer={
      <>
        <button className="btn btn--secondary" onClick={onClose}>Cancel</button>
        <button className="btn btn--danger" onClick={() => { onConfirm(row); onClose(); }}>
          <Icon name="trash" size={13} /> Delete
        </button>
      </>
    }>
    <p className="hccb-p">
      Delete <b>{row.name}</b> (ID {row.id})? The rule stops generating payments from the next cron run.
    </p>
    <p className="hccb-p hccb-p--muted">
      Payments already written to <code>commission_cashback_payment</code> for this rule are a separate table and are not
      described as cascading — the reference documents no cascade either way, so what happens to historic payment rows here is <b>UNCLEAR</b>.
    </p>
  </HccbModal>
);

/* ================================================================== */
const CommissionCashback = () => {
  const [rows, setRows] = hccbUseState(HCCB_SEED_ROWS);
  /* Filters apply on the Search button (#kt_search), not on keyup — hence the
     draft/applied split, same as the other generic-shell screens. */
  const [draft, setDraft] = hccbUseState({ id: "", skins: [] });
  const [applied, setApplied] = hccbUseState({ id: "", skins: [] });
  /* Real listing is always commission_cashbacks.id DESC (see header divergence 1). */
  const [sort, setSort] = hccbUseState({ key: "id", dir: "desc" });
  const [page, setPage] = hccbUseState(0);
  const [pageSize, setPageSize] = hccbUseState(50);
  const [form, setForm] = hccbUseState(null);   // null | { row: row|null }
  const [del, setDel] = hccbUseState(null);     // null | row

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "Exact ID", width: 160,
      tip: <>Exact match on <code>commission_cashbacks.id</code> — not a contains search.</> },
    { key: "skins", label: "Skins", type: "multi", icon: "flag", placeholder: "- All -", grow: true,
      options: HCCB_SKINS.map(s => ({ value: s.id, label: s.name })),
      tip: <>Server-side this is a <code>whereHas('skins', …)</code> over the <code>commission_cashback_skins</code> pivot, matching any of the chosen ids.</> },
  ];
  /* Faithful absence: the rows() scaffolding for date / active / auto_pay
     filters is commented out (L73-96), so those are NOT offered here. */

  const filtered = hccbUseMemo(() => {
    const idq = String(applied.id || "").trim();
    const sk = (applied.skins || []).map(Number);
    return rows.filter(r => {
      if (idq && String(r.id) !== idq) return false;
      if (sk.length && !r.skins.some(s => sk.indexOf(Number(s)) !== -1)) return false;
      return true;
    });
  }, [rows, applied]);

  const sorted = hccbUseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    const val = (r) => {
      switch (sort.key) {
        case "name": return r.name.toLowerCase();
        case "skins": return r.skins.map(hccbSkinName).join(", ").toLowerCase();
        case "cats": return r.gameCats.map(hccbCatName).join(", ").toLowerCase();
        case "start": return r.start;
        case "end": return r.end;
        case "periodicity": return hccbPeriodicityLabel(r.periodicity).toLowerCase();
        case "days": return hccbDaysLabel(r).toLowerCase();
        case "every": return r.every;
        case "balanceType": return hccbBalanceLabel(r.balanceType).toLowerCase();
        case "active": return r.active ? 1 : 0;
        case "autoPay": return r.autoPay ? 1 : 0;
        default: return r.id;
      }
    };
    return filtered.slice().sort((a, b) => {
      const va = val(a), vb = val(b);
      if (va === vb) return (a.id - b.id) * dir;
      return (va > vb ? 1 : -1) * dir;
    });
  }, [filtered, sort]);

  const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
  const safePage = Math.min(page, pageCount - 1);
  const paged = sorted.slice(safePage * pageSize, safePage * pageSize + pageSize);

  const onSearch = (v) => { setApplied({ id: v.id || "", skins: v.skins || [] }); setPage(0); };
  const onReset = () => { setDraft({ id: "", skins: [] }); setApplied({ id: "", skins: [] }); setPage(0); };

  const onSave = (r) => {
    if (r.id == null) {
      const id = rows.reduce((m, x) => Math.max(m, x.id), 0) + 1;
      setRows(rs => [{ ...r, id }, ...rs]);
      hccbToast(`Commission cashback "${r.name}" created`, `${r.skins.length} skin(s) · ${r.gameCats.length} game category(ies) · ${hccbPeriodicityLabel(r.periodicity)} every ${r.every} · ${r.cashbackPercentage}% of GGR.`);
    } else {
      setRows(rs => rs.map(x => x.id === r.id ? { ...x, ...r } : x));
      hccbToast(`Commission cashback "${r.name}" saved`, `Pivots synced: skins, game categories and ${r.days.length} period day(s).`);
    }
  };

  const onDelete = (r) => {
    setRows(rs => rs.filter(x => x.id !== r.id));
    hccbToast(`Commission cashback "${r.name}" deleted`, "The rule will not be picked up by the next payCommissionCashbacks run.");
  };

  const acts = (r) => (
    <div className="hccb-acts">
      <button className="hccb-act hccb-act--danger" title="Delete" onClick={(e) => { e.stopPropagation(); setDel(r); }}>
        <Icon name="trash" size={13} />
      </button>
      <button className="hccb-act hccb-act--edit" title="Edit" onClick={(e) => { e.stopPropagation(); setForm({ row: r }); }}>
        <Icon name="edit" size={13} />
      </button>
    </div>
  );

  /* Column set and display order are exactly the controller's (L23-35). */
  const columns = [
    { key: "id", label: "ID", sortable: true, firstDir: "desc", width: 84,
      render: r => <span className="hccb-id">{r.id}</span> },
    { key: "name", label: "Name", sortable: true, firstDir: "asc",
      render: r => (
        <button className="hccb-namelink" title="Edit" onClick={() => setForm({ row: r })}>
          <span>{r.name}</span><Icon name="chevron_right" size={13} />
        </button>
      ) },
    { key: "skins", label: "Skins", sortable: true,
      render: r => <HccbChips items={r.skins.map(hccbSkinName)} title={r.skins.map(hccbSkinName).join(", ")} /> },
    { key: "cats", label: "Game categories", sortable: true,
      render: r => <HccbChips items={r.gameCats.map(hccbCatName)} /> },
    { key: "start", label: "Start date", sortable: true, align: "center", width: 116,
      render: r => <span className="hccb-date">{hccbDmyIso(r.start)}</span> },
    { key: "end", label: "End date", sortable: true, align: "center", width: 116,
      render: r => <span className="hccb-date">{hccbDmyIso(r.end)}</span> },
    { key: "periodicity", label: "Periodicity", sortable: true, align: "center", width: 110,
      render: r => hccbPeriodicityLabel(r.periodicity) },
    { key: "days", label: "Days", sortable: true,
      render: r => {
        const l = hccbDaysLabel(r);
        return l === "" ? <span className="hccb-muted">—</span> : <span className="hccb-days">{l}</span>;
      } },
    { key: "every", label: "Every", sortable: true, align: "center", width: 82,
      render: r => <span className="hccb-num">{r.every}</span> },
    { key: "balanceType", label: "Balance type", sortable: true, width: 168,
      render: r => hccbBalanceLabel(r.balanceType) },
    { key: "active", label: "Active", sortable: true, align: "center", width: 90,
      render: r => <HccbYesNo on={r.active} /> },
    /* backend.extras.auto_pay = "Auto Pay" */
    { key: "autoPay", label: "Auto Pay", sortable: true, align: "center", width: 100,
      render: r => <HccbYesNo on={r.autoPay} /> },
    { key: "_acts", label: "Actions", align: "center", width: 116, render: acts },
  ];

  return (
    <HrsShell
      title="Commission Cashback"        /* backend.commission_cashback — label inferred */
      subtitle="Rules that pay players back a percentage of the GGR they generated, per skin, per game category, per period"
      gate={<>Real-platform access: <b>Super Admin only</b>. <code>CommissionCashbackPolicy</code> gates <code>viewAny</code>, <code>view</code>, <code>create</code>, <code>update</code> and <code>delete</code> on <code>isadmin()</code> alone — there is no Customer-Care permission, no skin scoping and no per-row condition anywhere on this screen. </>}
      gateNote={<>Two honest caveats: <code>store()</code> authorizes the <b>create</b> ability even when it is updating an existing row, so an admin variant that could create but not edit would not exist today; and the preview endpoint the form calls, <code>GET /commission_cashback/payment/info</code>, has <b>no authorize call at all</b>. A dead non-admin branch in <code>rows()</code> filters the skins pivot without joining it and would raise a SQL error — unreachable, because the policy blocks non-admins first.</>}
      explainer={
        <>
          <Explainer compact title="What this screen configures, in plain English" bullets={[
            <>A <b>commission cashback</b> pays a player back a percentage of the house's <b>GGR</b> on them. The base amount is <code>SUM(players_report.profit)</code> for that player over the period, floored by <b>min base amount</b> and capped by <b>max base amount</b>.</>,
            <>This screen only <b>defines the rules</b>. Nothing is paid from here: the cron <code>GET /cronjobs/payCommissionCashbacks</code> queues <code>PayCommissionCashbacks</code>, which calls <code>CommissionCashback::pay($date)</code> and writes rows into <code>commission_cashback_payment</code> — the <b>Payments</b> tab above.</>,
            <>The cron skips any period that already carries an <b>admin-edited</b> payment, and nets off amounts already paid, so re-running it does not double-pay. With <b>Auto Pay</b> on, generated payments are settled immediately instead of waiting for an operator.</>,
            <>Payment credits land in the wallet named by <b>Balance type</b> — <code>balance_withdrawable</code> or <code>balance</code> — and are recorded in transaction history as type <code>121</code> (Commission Cashback).</>,
            <>Scope is set by two pivots: <b>Skins</b> and <b>Game categories</b> (only Casino, Casino Live and Virtual are selectable). The schedule is <b>Periodicity</b> + <b>Repeat it every</b> + the selected <b>Days</b>.</>,
          ]} />
          <Explainer compact title="This screen has no menu entry — it is reachable only by URL"
            bullets={[
              <>The sidebar link (<code>sidebar.blade.php</code> L905-912, guarded <code>@if( isadmin() )</code>) sits inside a commented-out <code>&lt;?/* … */?&gt;</code> block spanning L890-913, so it renders for <b>nobody</b> — not even a super admin.</>,
              <>The only way to reach the real screen is to type <code>/commission_cashback</code>. Its sibling screens in that same block (Lobbies, Acca bonus, Acca bonus payments) are orphaned the same way.</>,
              <>The feature is otherwise fully live: the routes are registered, the policy passes for admins, and the <code>payCommissionCashbacks</code> cron keeps generating payments for whatever rules already exist — so a rule left behind here goes on paying out with no navigable place to review it.</>,
            ]} />
        </>
      }
      actions={
        <button className="hrs-btn hrs-btn--filters" onClick={() => setForm({ row: null })}>
          {/* backend.new_commission_cashback — label inferred */}
          <Icon name="plus" size={14} /> New Commission Cashback
        </button>
      }>

      <HccbTabs />

      <HrsFilters
        fields={FIELDS} values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={onSearch} onReset={onReset}
        resultLabel={`${hrsInt(sorted.length)} of ${hrsInt(rows.length)}`} />

      <HrsTable
        columns={columns} rows={paged} rowKey="id"
        sort={sort} onSort={(s) => { setSort(s); setPage(0); }}
        empty={(applied.id || (applied.skins || []).length)
          ? "No commission cashback matches these filters."
          : "No commission cashback rules yet — create one with New Commission Cashback."}
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <span className="hccb-cardpct">{r.cashbackPercentage}%</span>
            </div>
            <div className="hccb-cardmeta">
              <span className="hccb-id">ID {r.id}</span>
              <HccbYesNo on={r.active} />
              {r.autoPay && <span className="hccb-chip hccb-chip--auto">Auto Pay</span>}
            </div>
            <div className="hrs-card__grid">
              <span>Period</span><b>{hccbDmyIso(r.start)} → {hccbDmyIso(r.end)}</b>
              <span>Schedule</span><b>{hccbPeriodicityLabel(r.periodicity)} · every {r.every}</b>
              <span>Balance type</span><b>{hccbBalanceLabel(r.balanceType)}</b>
            </div>
            <details className="hccb-more">
              <summary>More</summary>
              <div className="hrs-card__grid">
                <span>Skins</span><b>{r.skins.map(hccbSkinName).join(", ")}</b>
                <span>Game categories</span><b>{r.gameCats.map(hccbCatName).join(", ")}</b>
                <span>Days</span><b>{hccbDaysLabel(r) || "—"}</b>
                <span>Min base</span><b>{r.minBase == null ? "—" : hccbNum(r.minBase)}</b>
                <span>Max base</span><b>{r.maxBase == null ? "—" : hccbNum(r.maxBase)}</b>
              </div>
            </details>
            <div className="hccb-card__acts">
              <button className="btn btn--secondary btn--sm" onClick={() => setForm({ row: r })}><Icon name="edit" size={12} /> Edit</button>
              <button className="btn btn--ghost btn--sm hccb-card__del" onClick={() => setDel(r)}><Icon name="trash" size={12} /> Delete</button>
            </div>
          </>
        )} />

      <HrsPager page={safePage} pageSize={pageSize} total={sorted.length} sizes={HCCB_PAGE_SIZES}
        onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(0); }} />

      <HrsSection title="Real-platform findings"
        sub="Behaviour of the live screen that this rebuild reports rather than quietly repairs.">
        <div className="hccb-findings">
          <HccbFinding tone="err" title="The screen is orphaned navigation">
            Its sidebar entry is inside a commented-out block (<code>sidebar.blade.php</code> L890-913), so no role ever
            sees the link. Only a typed <code>/commission_cashback</code> URL reaches it, while the cron keeps paying out
            whatever rules exist.
            {/* <!-- SUGGESTION: either re-enable the CMS ▾ entries in that block or retire the feature — a live payout
                 engine with no navigable configuration screen is the worst of both. --> */}
          </HccbFinding>

          <HccbFinding title="Sorting is advertised but never happens">
            Every column except Actions is marked orderable, yet <code>rows()</code> has its whole order-by mapping
            commented out and always answers <code>id DESC</code>. This rebuild sorts for real (default <code>id DESC</code>)
            rather than shipping headers that lie.
            {/* <!-- SUGGESTION: restore the order-by mapping in CommissionCashbackController::rows, or set
                 orderable:false on the headers. --> */}
          </HccbFinding>

          <HccbFinding title="Edit validates this row's id against the wrong table">
            On the update path <code>store()</code> sets <code>$rules['id'] = 'exists:mobile_validators'</code> — a
            copy-paste from another generic screen. Whether an edit saves depends on an unrelated table's ids. The
            evident intent (the id must be an existing commission cashback) is what this rebuild enforces.
            {/* <!-- SUGGESTION: change store() L192 to exists:commission_cashbacks,id. --> */}
          </HccbFinding>

          <HccbFinding title="store() authorizes create when it is updating">
            The update path calls <code>authorize('create', …)</code>, not <code>update</code>. Both resolve to
            <code>isadmin()</code> today, so nothing breaks — but the two abilities can never diverge.
            {/* <!-- SUGGESTION: authorize 'update' when an id is present, so the policy's own update ability
                 becomes meaningful. --> */}
          </HccbFinding>

          <HccbFinding title="The show route points at a method that does not exist">
            <code>GET /commission_cashback/{"{id}"}</code> is registered but <code>CommissionCashbackController</code> has
            no <code>show()</code>; hitting it errors. The non-admin branch of the row JS links names straight there — dead
            code, since <code>viewAny</code> already blocks non-admins.
            {/* <!-- SUGGESTION: drop the show route and the non-admin JS branch, or implement a read-only show(). --> */}
          </HccbFinding>

          <HccbFinding title="Selected days are stored without any validation">
            <code>days[]</code> carries no rules at all: whatever the client posts is written to
            <code>commission_cashback_period_days</code>. Weekday values outside 1-7 and month days outside 1-31 are
            accepted and would silently never fire.
            {/* <!-- SUGGESTION: validate days[] as integers, bounded 1-7 for weekly and 1-31 for monthly, and reject
                 days when periodicity is daily. --> */}
          </HccbFinding>

          <HccbFinding title="The schedule preview endpoint has no gate">
            The form's next-payments list comes from <code>GET /commission_cashback/payment/info</code>, the one route in
            this group with no <code>authorize()</code> call — readable by any authenticated, 2FA'd back-office user.
            {/* <!-- SUGGESTION: authorize the info endpoint against CommissionCashbackPolicy::viewAny. --> */}
          </HccbFinding>

          <HccbFinding title="apply_over can only ever be GGR">
            <code>store()</code> hardcodes <code>'ggr'</code> and never reads a posted value. The schema's
            <code>turnover</code> alternative reaches an unimplemented TODO branch in <code>CommissionCashback::pay()</code>,
            so it is correctly not offered in the form.
            {/* <!-- SUGGESTION: drop the turnover enum value from the column until the pay() branch is implemented,
                 so the schema stops promising a mode that does not exist. --> */}
          </HccbFinding>

          <HccbFinding title="A dead non-admin branch would SQL-error">
            <code>rows()</code> filters <code>commission_cashback_skins.skin_id</code> for non-admins without joining that
            pivot. The policy blocks every non-admin before that branch runs, so it never executes — but it is a trap for
            anyone who later loosens the policy.
            {/* <!-- SUGGESTION: delete the unreachable non-admin branch in rows(), or add the missing join before the
                 policy is ever relaxed. --> */}
          </HccbFinding>
        </div>
      </HrsSection>

      {form && <HccbFormModal row={form.row} onClose={() => setForm(null)} onSave={onSave} />}
      {del && <HccbDeleteDialog row={del} onClose={() => setDel(null)} onConfirm={onDelete} />}
    </HrsShell>
  );
};

window.CommissionCashback = CommissionCashback;
