// Represents: GET /vendorsgroups · VendorsGroupsController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Vendors Groups"
/* Settings ▾ → Vendors Groups. Screen actions: VendorsGroupsController::index (L16, aborts 404
   unless isadmin()), ::getVendorsGroupsTable (L41, the DataTables feed), ::vendorgroupForm (L136,
   GET /vendorsgroups/form/?id=) and ::saveVendorGroup (L154, POST /vendorsgroups/saveVendorGroup?id=).
   Routes routes/admin.php:L1191-1205 — all unnamed, inside Route::name('admin.')
   ->middleware(['auth','admin','2fa','g2fa']). Views: admin/vendorsgroups/index.blade.php +
   modals/vendorgroup.blade.php (generaModalGestione) + forms/vendorgroup.blade.php;
   JS driver public/js/pages/vendorsgroups/ajax.js.

   A "vendor group" collapses several `providers` rows that belong to one vendor into a single
   billing unit. Data model: `vendorsgroups` (model VendorGroup — fillable `name`,
   `base_provider_id`, unix addedTime/updateTime) ⟷ `vendorsgroups_providers` pivot (model
   VendorGroupProvider, synced on save by updateVendorgroupProviders(): subscribe new /
   unsubscribe removed) ⟷ `providers`.

   Cross-checked with the Business report (docs/ISYSTEM_REFERENCE.md §Batch 2 "Business report" and
   src/pages/HostReportBusiness.jsx), the ONLY consumer of this table: ReportsController::
   getBusinessReport loads VendorGroup::with('providers') (:8379), excludes every grouped provider
   from the flat provider list (:8395-8397, :8437), renders each group as one expandable `group_<id>`
   row with "└" sub-provider rows, prices the WHOLE group's Base cost with the base provider's
   users_providers.percentage (:7954-7959, :8071-8073), and charges the group's Premium only when the
   group's total profit for the period is positive (:8172). The mock groups below are therefore kept
   consistent with HostReportBusiness.jsx: groups 1-3 (Pragmatic Play / Amusnet / 3 Oaks) are byte-for-
   byte the same names, members and base providers as its HRBZ_GROUPS, and no group here contains a
   provider that page lists as a single (Sportsbook 69, Amigo Gaming 131 … Wazdan 139). Groups 4-6
   exist in this global table but do not appear in that page's report because the report is per skin
   and per period — a group whose providers saw no play on the selected skin produces no rows.

   Known real-platform defects, handled per the repo's known-bug policy (CLAUDE.md):
   - Name filter is DEAD: getVendorsGroupsTable compares $column["name"] == "name_en" (L67-69) while
     index registers the column as `name` (L24), so the Name box never filters. Evident intent
     implemented here (case-insensitive contains) — see the field comment + SUGGESTION.
   - Delete route is UNREGISTERED: the trash icon builds /vendorsgroups/delete/<id>/ but no such
     route exists in routes/, so it 404s; VendorsGroupsController::delete (L218-229) is unreachable
     dead code. The action exists in the real UI, so it is kept here and simulated honestly — the
     confirm dialog reports the 404 and the group is NOT removed. See SUGGESTION at HvgDeleteDialog.

   Faithful to the real screen, deliberately NOT added: no KPIs/totals (n/a), no export (none), no
   bulk actions (n/a), no status column (the controller's stati() 0/1 Disabled/Active is dead code
   never called for this screen, L30-39), no `tipologieVendorsgroups()` type picker (dead, L122-134),
   and no Providers column — the real table is ID / Name / Actions only (membership is edited in the
   modal). The row payload also carries a hidden `nome_completo` field (JS-escaped name, L107) used
   only to build the edit link's title; nothing renders it.

   Other quirks recorded but not reproduced as bugs: the Name cell's outer <div> is never closed
   (L105); the JS actions renderer reads full['ID'] and only works because __('backend.id') happens to
   resolve to exactly "ID"; tinymce + datepickers are loaded on the page but unused;
   getVendorsgroups($cat) (L232-240) is syntactically broken SQL with no callers.

   <!-- SUGGESTION: gate getVendorsGroupsTable / vendorgroupForm / saveVendorGroup with the same isadmin() check index() already uses. Today the list page is super-admin-only but its data, form and save endpoints are open to every authenticated 2FA'd back-office user, so the 404 on index protects nothing. -->
   <!-- SUGGESTION: the 2026_06_24_155202 migration declares vendorsgroups.provider_id as a NOT NULL unsignedInteger with no default, but it is not in VendorGroup::$fillable and nothing ever writes it — drop the column (or give it a default) before a strict-mode MySQL turns VendorGroup::create() into a 500. -->
   <!-- SUGGESTION: name the four /vendorsgroups routes (admin.vendorsgroups.index/table/form/save); they are the only Settings screen whose blades/JS have to hardcode literal URLs because route() is unusable. --> */

const { useState: hvgUseState, useMemo: hvgUseMemo } = React;

/* Deterministic PRNG (FNV-1a + mulberry32) — same convention as the sibling Host pages, so the
   mock list renders identically on every load. */
const hvgHash = (s) => { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
const hvgRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
/* Provider::all() — the form's picker is UNSCOPED (unlike the Business report, which uses
   Provider::active(), scope stato = 1), so a group can legitimately contain a provider that never
   shows up in any report. Ids/names are mock operator data; ids 69/101-139 are aligned with
   HostReportBusiness.jsx. */
const HVG_PROVIDERS = [
  { id: 69, name: "Sportsbook" },
  { id: 101, name: "Pragmatic Play Slots" }, { id: 102, name: "Pragmatic Play Live" },
  { id: 111, name: "Amusnet" }, { id: 112, name: "Amusnet Live" },
  { id: 121, name: "3Oaks" }, { id: 122, name: "3Oaks P" },
  { id: 131, name: "Amigo Gaming" }, { id: 132, name: "BGaming" }, { id: 133, name: "Belatra" },
  { id: 134, name: "Evolution" }, { id: 135, name: "Hacksaw" }, { id: 136, name: "NetEnt" },
  { id: 137, name: "Novomatic" }, { id: 138, name: "Playson" }, { id: 139, name: "Wazdan" },
  { id: 141, name: "Ezugi" }, { id: 142, name: "EzugiX" }, { id: 143, name: "EzugiZ" },
  { id: 151, name: "Spearhead" }, { id: 161, name: "Creedz" },
  { id: 171, name: "Iconix" }, { id: 172, name: "Classic Casino" }, { id: 173, name: "Rocketman" },
  { id: 174, name: "Vivo Live" }, { id: 175, name: "Macaw" }, { id: 176, name: "Fazi" },
  { id: 177, name: "Play'n GO" }, { id: 178, name: "Quickspin" }, { id: 179, name: "Spribe" },
  { id: 180, name: "Habanero" }, { id: 181, name: "PG Soft" }, { id: 182, name: "Endorphina" },
  { id: 183, name: "Booming Games" }, { id: 184, name: "Big Time Gaming" },
];
const hvgProvider = (id) => HVG_PROVIDERS.find(p => p.id === Number(id)) || null;
const hvgProviderName = (id) => { const p = hvgProvider(id); return p ? p.name : `#${id}`; };

/* `vendorsgroups` rows. Groups 1-3 pin their base_provider_id to match HostReportBusiness.jsx
   exactly; for the rest the base provider is drawn from the group's own members by the deterministic
   PRNG (base_provider_id is a pure contract choice — any member can price the group). */
/* ---------- the row source ------------------------------------------------
   Was six invented groups (Pragmatic Play, Amusnet, 3 Oaks...) with a seeded
   RNG picking which member was the base provider. Now vendor_groups, live,
   with its members joined through vendor_group_providers.

   `base` is a real column here (vendor_groups.base_provider_id) rather than a
   random pick — isystem stores it too; the generator was guessing because it
   had nothing to read. */
const hvgRow = (g) => ({
  id: g.id,
  name: g.name,
  members: (g.members || []).map(m => m.provider_id),
  memberNames: (g.members || [])
    .map(m => (m.providers && m.providers.name) || String(m.provider_id)),
  base: g.base_provider_id,
  baseName: (g.base_provider && g.base_provider.name) || null,
});

const HVG_PAGE_SIZES = [5, 10, 25, 50]; // ajax.js lengthMenu [5,10,25,50], pageLength 50

/* ------------------------------------------------------------------ *
 * Modal chrome — shared .bp-modal, full-screen on mobile (brief §11).
 * The real modal is generaModalGestione() → admin/utils/modal.blade.php
 * (modal gestioneVendorGroupModal, form gestioneVendorGroupForm, body
 * AJAX-loaded from /vendorsgroups/form, callback callbackVendorsgroups).
 * ------------------------------------------------------------------ */
const HvgModal = ({ title, onClose, children, footer, wide }) => (
  <div className="bp-modal-scrim hvg-scrim" onClick={onClose}>
    <div className={`bp-modal hvg-modal${wide ? " hvg-modal--wide" : ""}`} onClick={e => e.stopPropagation()}>
      <div className="hvg-modal__head">
        <div className="hvg-modal__title">{title}</div>
        <button className="hvg-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hvg-modal__body">{children}</div>
      {footer && <div className="hvg-modal__foot">{footer}</div>}
    </div>
  </div>
);

/* ------------------------------------------------------------------ *
 * Dual-list provider picker — the real form uses the `loumultiselect`
 * widget with a search box on BOTH sides (forms/vendorgroup.blade.php
 * L36-44), preselected from vendorsgroups_providers where
 * vendorgroup_id = row.id (L39). Clicking an entry moves it across.
 * ------------------------------------------------------------------ */
const HvgDualPicker = ({ value, base, onChange, invalid }) => {
  const [qa, setQa] = hvgUseState("");
  const [qs, setQs] = hvgUseState("");
  const sel = value.map(Number);
  const hit = (p, q) => !q || p.name.toLowerCase().includes(q.trim().toLowerCase());
  const available = HVG_PROVIDERS.filter(p => sel.indexOf(p.id) === -1 && hit(p, qa));
  const chosen = HVG_PROVIDERS.filter(p => sel.indexOf(p.id) !== -1 && hit(p, qs));
  const add = (id) => onChange([...sel, id]);
  const drop = (id) => onChange(sel.filter(x => x !== id));

  return (
    <div className={`hvg-dual${invalid ? " hvg-dual--err" : ""}`}>
      <div className="hvg-pane">
        <div className="hvg-pane__head">
          <span>Available providers</span>{/* label inferred */}
          <span className="hvg-count">{available.length}</span>
        </div>
        <input className="input hvg-pane__search" placeholder="Search…" value={qa} onChange={e => setQa(e.target.value)} />
        <div className="hvg-pane__list">
          {available.length === 0 && <div className="hvg-pane__empty">No providers match.</div>}
          {available.map(p => (
            <button key={p.id} type="button" className="hvg-opt" onClick={() => add(p.id)} title={`Add ${p.name}`}>
              <span className="hvg-opt__n">{p.name}</span>
              <Icon name="plus" size={12} />
            </button>
          ))}
        </div>
        <button type="button" className="hvg-bulk" disabled={available.length === 0}
          onClick={() => onChange([...sel, ...available.map(p => p.id)])}>Add all shown</button>
      </div>

      <div className="hvg-swap"><Icon name="arrow_down_up" size={16} style={{ transform: "rotate(90deg)" }} /></div>

      <div className="hvg-pane">
        <div className="hvg-pane__head">
          <span>Selected providers</span>{/* label inferred */}
          <span className="hvg-count">{chosen.length}</span>
        </div>
        <input className="input hvg-pane__search" placeholder="Search…" value={qs} onChange={e => setQs(e.target.value)} />
        <div className="hvg-pane__list">
          {chosen.length === 0 && <div className="hvg-pane__empty">No provider selected yet.</div>}
          {chosen.map(p => (
            <button key={p.id} type="button" className="hvg-opt hvg-opt--sel" onClick={() => drop(p.id)} title={`Remove ${p.name}`}>
              <span className="hvg-opt__n">
                {p.name}
                {String(base) === String(p.id) && <span className="hvg-basechip" title="base_provider_id — this provider's users_providers.percentage prices the whole group's Base cost in the Business report">base</span>}
              </span>
              <Icon name="x" size={12} />
            </button>
          ))}
        </div>
        <button type="button" className="hvg-bulk" disabled={chosen.length === 0}
          onClick={() => onChange(sel.filter(id => chosen.every(c => c.id !== id)))}>Remove all shown</button>
      </div>
    </div>
  );
};

/* ------------------------------------------------------------------ *
 * Create / Edit form — GET /vendorsgroups/form/?id=<id> then POST
 * /vendorsgroups/saveVendorGroup?id=<id> (@csrf). Section header "Data"
 * (backend.generic_data). Neither endpoint has ANY permission check.
 * ------------------------------------------------------------------ */
const HvgFormModal = ({ group, onClose, onSave }) => {
  const isNew = !group;
  const [name, setName] = hvgUseState(group ? group.name : "");
  const [members, setMembers] = hvgUseState(group ? group.members.slice() : []);
  const [base, setBase] = hvgUseState(group && group.base != null ? String(group.base) : "");
  const [errs, setErrs] = hvgUseState({});   // "campierrati" — the fields the server flags
  const [banner, setBanner] = hvgUseState("");

  const save = () => {
    /* Mirrors the inline validation in saveVendorGroup L171-202 (there is no FormRequest). The real
       endpoint answers ajaxError($message, ["campierrati" => [fields]]) — one message plus the list
       of offending fields, which the shared modal turns into red field outlines. */
    const e = {};
    if (!name.trim()) e.name = "Insert name";
    if (!base) e.base_provider_id = "Select base provider";
    if (!members.length) e.providers_list = "Select at least one provider";
    if (base && members.length && members.indexOf(Number(base)) === -1) e.base_provider_id = "Base provider not in the list!";
    setErrs(e);
    const first = ["name", "base_provider_id", "providers_list"].map(k => e[k]).filter(Boolean)[0];
    if (first) { setBanner(first); return; }
    setBanner("");
    onSave({ id: isNew ? null : group.id, name: name.trim(), members: members.map(Number), base: Number(base) });
    onClose();
  };

  const clearErr = (k) => { setErrs(x => { const n = { ...x }; delete n[k]; return n; }); setBanner(""); };

  return (
    <HvgModal
      wide
      /* label inferred — the real modal is opened with the leftover Italian "new" title param
         "Nuovo VendorGroup", although the toolbar button that opens it says "New Vendor Group"
         (index.blade.php:58-62). Edit titles are already English: "Edit <name>" (controller L105). */
      title={isNew ? <>New Vendor Group{/* label inferred */}</> : `Edit ${group.name}`}
      onClose={onClose}
      footer={<>
        <button className="btn btn--secondary" onClick={onClose}>Close</button>
        <button className="btn btn--primary" onClick={save}><Icon name="check" size={13} /> Save</button>
      </>}>

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

      <div className="hvg-sectitle">Data</div>{/* backend.generic_data */}

      <div className="hvg-field">
        <label className="hvg-label" htmlFor="hvg-name">Name <span className="hvg-req">*</span></label>
        <input id="hvg-name" className={`input${errs.name ? " hvg-invalid" : ""}`} style={{ width: "100%" }} autoFocus
          value={name} onChange={e => { setName(e.target.value); clearErr("name"); }} />
        {errs.name && <div className="hvg-fielderr">{errs.name}</div>}
      </div>

      <div className="hvg-field">
        <label className="hvg-label">Select Providers <span className="hvg-req">*</span></label>{/* label inferred */}
        <HvgDualPicker value={members} base={base} invalid={!!errs.providers_list}
          onChange={(v) => { setMembers(v); clearErr("providers_list"); if (errs.base_provider_id) clearErr("base_provider_id"); }} />
        {errs.providers_list && <div className="hvg-fielderr">{errs.providers_list}</div>}
        <div className="hvg-hint">
          Saving syncs the <code>vendorsgroups_providers</code> pivot through
          <code> VendorGroupProvider::updateVendorgroupProviders()</code> — newly ticked providers are subscribed,
          unticked ones unsubscribed. A provider inside a group is <b>removed from the flat provider list</b> of the
          Business report and only ever counted under its group row.
        </div>
      </div>

      <div className="hvg-field">
        <label className="hvg-label" htmlFor="hvg-base">Base Provider <span className="hvg-req">*</span></label>{/* label inferred */}
        {/* Faithful to the real select2: it is fed the FULL provider list, not just the ticked ones,
            which is exactly why saveVendorGroup needs the "Base provider not in the list!" check
            (L190-192). allowClear → the empty placeholder option below. */}
        <select id="hvg-base" className={`select${errs.base_provider_id ? " hvg-invalid" : ""}`} style={{ width: "100%" }}
          value={base} onChange={e => { setBase(e.target.value); clearErr("base_provider_id"); }}>
          <option value="">Select Base Provider</option>
          {HVG_PROVIDERS.map(p => (
            <option key={p.id} value={p.id}>{p.name}{members.indexOf(p.id) === -1 ? " — not in this group" : ""}</option>
          ))}
        </select>
        {errs.base_provider_id && <div className="hvg-fielderr">{errs.base_provider_id}</div>}
        <div className="hvg-hint">
          The base provider prices the <b>whole</b> group: in the Business report every member's Base cost is computed with
          this provider's <code>users_providers.percentage</code> (the skin admin's contract row), not with its own.
          Premium cost stays per provider but is only charged when the group's total profit for the period is positive.
        </div>
      </div>
    </HvgModal>
  );
};

/* ------------------------------------------------------------------ *
 * Delete — the trash icon calls deleteConfirm('/vendorsgroups/delete/<id>/',
 * …, refreshVendorsgroups) but NO such route is registered in routes/, so
 * the request 404s and VendorsGroupsController::delete (L218-229) never
 * runs. The action is part of the real UI, so it is kept and simulated
 * honestly: confirm → 404 → the row stays.
 * <!-- SUGGESTION: register GET /vendorsgroups/delete/{id} (gated isadmin(), like the controller method already expects) and make delete() also clear the vendorsgroups_providers pivot rows — as written it would hard-delete the group row only and orphan its pivot rows. Until the route exists, hide the trash icon instead of shipping a link that always 404s. -->
 * ------------------------------------------------------------------ */
const HvgDeleteDialog = ({ group, onClose }) => {
  const [failed, setFailed] = hvgUseState(false);
  const url = `/vendorsgroups/delete/${group.id}/`;
  const run = () => {
    setFailed(true);
    hrsToast(`Delete failed — 404 ${url}`, "The delete route is not registered in routes/admin.php, so VendorsGroupsController::delete() is never reached. Nothing was deleted.");
  };
  return (
    <HvgModal title={failed ? "Delete failed" : "Delete vendor group"} onClose={onClose}
      footer={failed
        ? <button className="btn btn--secondary" onClick={onClose}>Close</button>
        : <>
          <button className="btn btn--secondary" onClick={onClose}>Cancel</button>
          <button className="btn btn--danger" onClick={run}><Icon name="trash" size={13} /> Delete</button>
        </>}>
      {!failed ? (
        <>
          <div className="hvg-dlgq">Delete <b>{group.name}</b> (ID {group.id})?</div>
          <div className="hvg-hint">
            The real UI sends a plain <code>GET {url}</code> behind a JS confirm — the platform's generic delete
            convention for this batch of screens.
          </div>
        </>
      ) : (
        <>
          <div className="hvg-err"><Icon name="alert" size={13} /> 404 Not Found — <code>GET {url}</code></div>
          <div className="hvg-hint">
            <b>{group.name}</b> was <b>not</b> deleted, and this is exactly what happens on the real platform: the trash
            icon builds that URL in <code>public/js/pages/vendorsgroups/ajax.js</code>, but no matching route is declared
            in any file under <code>routes/</code>. <code>VendorsGroupsController::delete()</code> (L218-229) is
            unreachable dead code — and even if it were routed it would delete the <code>vendorsgroups</code> row only,
            orphaning its <code>vendorsgroups_providers</code> pivot rows.
          </div>
          <div className="hvg-hint">
            Groups can still be emptied in practice by editing them, but the group row itself cannot be removed from the
            back office today.
          </div>
        </>
      )}
    </HvgModal>
  );
};

/* ------------------------------------------------------------------ */
const SetVendorsGroups = () => {
  window.useLocale && window.useLocale();

  const feed = useHrsFetch(() => window.sb.list("vendorGroups", { limit: 200 }), []);
  const rows = hvgUseMemo(() => (feed.data || []).map(hvgRow), [feed.data]);
  /* Filters apply on the Search button (#kt_search), not on keyup — hence the draft/applied split. */
  const [draft, setDraft] = hvgUseState({ id: "", name: "" });
  const [applied, setApplied] = hvgUseState({ id: "", name: "" });
  /* JS default order [[0,"desc"]] → vendorsgroups.id DESC (the server's own fallback is id ASC when
     no order param arrives, controller L53). Actions is not orderable (L71-88). */
  const [sort, setSort] = hvgUseState({ key: "id", dir: "desc" });
  const [page, setPage] = hvgUseState(0);
  const [pageSize, setPageSize] = hvgUseState(50);
  const [form, setForm] = hvgUseState(null);   // null | { group: row|null }
  const [del, setDel] = hvgUseState(null);     // null | row

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "Exact ID", width: 150,
      tip: <>Exact match on <code>vendorsgroups.id</code> (controller L63-65) — not a contains search.</> },
    /* KNOWN BUG — DIVERGENCE: on the real platform this box does nothing. getVendorsGroupsTable
       tests $column["name"] == "name_en" (L67-69) while index() registers the column as `name`
       (L24), so the branch never fires and the Name filter is silently ignored. Per the repo's
       known-bug policy the evident intent is implemented here: case-insensitive contains on
       vendorsgroups.name. */
    // <!-- SUGGESTION: fix VendorsGroupsController::getVendorsGroupsTable L67 to compare against "name" (the column index() actually registers) — or rename the registered column to name_en — so the Name filter starts working. There is no name_en column on vendorsgroups at all. -->
    { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Group name", grow: true,
      tip: <>Dead on the real platform: the server looks for a column called <code>name_en</code> that this table never registers, so typing here changes nothing. Implemented here as the intended case-insensitive contains match on <code>vendorsgroups.name</code>.</> },
  ];

  const filtered = hvgUseMemo(() => {
    const idq = String(applied.id || "").trim();
    const nq = String(applied.name || "").trim().toLowerCase();
    return rows.filter(r => {
      if (idq && String(r.id) !== idq) return false;
      if (nq && r.name.toLowerCase().indexOf(nq) === -1) return false;
      return true;
    });
  }, [rows, applied]);

  const sorted = hvgUseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    return filtered.slice().sort((a, b) => (sort.key === "name"
      ? a.name.localeCompare(b.name) * dir
      : (a.id - b.id) * 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 || "", name: v.name || "" }); setPage(0); };
  /* The real toolbar has only the Search button; Reset here is the shell's own client-side clear
     (same action as the "Clear all" pill affordance), not an extra server control. */
  const onReset = () => { setDraft({ id: "", name: "" }); setApplied({ id: "", name: "" }); setPage(0); };

  /* Create and edit are writes across two tables — vendor_groups plus the
     vendor_group_providers pivot — and src/supabase.js is read-only by design
     (stage 7 of docs/WORK_PLAN.md). This used to push into a local array and
     toast "created", which survives nothing and reads as success. */
  const onSave = (g) => {
    const verb = g.id == null ? "insert" : "update";
    hrsToast("Not saved — no write path yet",
      `Would ${verb} vendor_groups (name "${g.name}", base_provider_id ${g.base ?? "null"}) and sync ${g.members.length} row(s) in vendor_group_providers. Reads are live; writes land in stage 7.`);
  };

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

  const columns = [
    { key: "id", label: "ID", sortable: true, firstDir: "desc", width: 96,
      render: r => <span className="hvg-id">{r.id}</span> },
    /* Name is rendered as the edit link — javascript:gestioneVendorGroup(id,'Edit <name>') with an
       "Edit" tooltip (controller L105). The row also carries a hidden nome_completo (JS-escaped
       name, L107) that only feeds that title; nothing displays it. */
    { key: "name", label: "Name", sortable: true, firstDir: "asc",
      render: r => (
        <button className="hvg-namelink" title="Edit" onClick={() => setForm({ group: r })}>
          <span>{r.name}</span>
          <Icon name="chevron_right" size={13} />
        </button>
      ) },
    { key: "_acts", label: "Actions", align: "center", width: 120, render: acts }, // backend.actions
  ];

  return (
    <HrsShell
      title="Vendors Groups" /* hardcoded English in the real sidebar, not a backend.* key */
      subtitle="Bill several provider integrations of one vendor as a single unit in the Business report"
      gate={<>Real-platform access: the page itself is <b>Super Admin only</b> — <code>VendorsGroupsController::index</code> aborts <code>404</code> unless <code>isadmin()</code> (user_level 0). The Settings ▾ entry has <b>no per-item gate</b>, so Skin admins and any skin with <code>enable_agents_operators</code> still see the menu link and land on that 404. </>}
      gateNote={<>Permission asymmetry, honestly: <code>getVendorsGroupsTable</code>, <code>vendorgroupForm</code> and <code>saveVendorGroup</code> carry <b>no role check at all</b> — any authenticated, 2FA'd back-office user who calls those URLs directly can list, create and edit vendor groups. Only <code>delete()</code> would re-check <code>isadmin()</code>, and it is not routed.</>}
      explainer={{ bullets: [
        <>A <b>vendor group</b> ties several <code>providers</code> rows that belong to the same vendor (e.g. a slots integration and its live-casino sibling) to one billing unit. Membership lives in the <code>vendorsgroups_providers</code> pivot.</>,
        <>The one place groups are consumed is the <b>Business report</b>: grouped providers disappear from the flat provider list and are reported as a single expandable row instead. The group's <b>base provider</b> decides the percentage used to price every member's Base cost, and the group's Premium cost is charged only when its total profit for the period is positive.</>,
        <>Groups are global — they are not per skin. A group only shows up in a report when its providers actually saw play on the skin and period being reported.</>,
        <><b>Delete does not work on the real platform.</b> The trash icon points at <code>/vendorsgroups/delete/&lt;id&gt;/</code>, a route that is not registered anywhere, so it answers 404 and the group survives. The button is kept here and behaves the same way rather than pretending to succeed.</>,
      ] }}
      actions={
        <button className="hrs-btn hrs-btn--filters" onClick={() => setForm({ group: null })}>
          <Icon name="plus" size={14} /> New Vendor Group
        </button>
      }>

      <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)}`} />

      {/* HrsAsync owns loading / error / empty. The error branch is the one that
          matters: signed out, RLS returns zero rows and a bare table would say
          "no vendor groups", which is wrong and unactionable. */}
      <HrsAsync state={feed} skeletonRows={6} skeletonCols={3}
                empty="No vendor groups yet. They are created on this screen once the write path exists.">
        {() => (<>
      <HrsTable
        columns={columns} rows={paged} rowKey="id"
        empty={applied.id || applied.name ? "No vendor group matches these filters." : "No vendor groups yet — create one with New Vendor Group."}
        sort={sort} onSort={(s) => { setSort(s); setPage(0); }}
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <span className="hvg-id">ID {r.id}</span>
            </div>
            <div className="hvg-card__acts">
              <button className="btn btn--secondary btn--sm" onClick={() => setForm({ group: r })}><Icon name="edit" size={12} /> Edit</button>
              <button className="btn btn--ghost btn--sm hvg-card__del" onClick={() => setDel(r)}><Icon name="trash" size={12} /> Delete</button>
            </div>
          </>
        )} />

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

      {form && <HvgFormModal group={form.group} onClose={() => setForm(null)} onSave={onSave} />}
      {del && <HvgDeleteDialog group={del} onClose={() => setDel(null)} />}
    </HrsShell>
  );
};

/* Loads after src/pages/HostSettings.jsx, deliberately replacing its legacy SetVendorsGroups global
   (app.jsx:468 renders <SetVendorsGroups/> for route key "settings-vendors"). */
window.SetVendorsGroups = SetVendorsGroups;
