// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /supportusers/ · UsersController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Support users (Operators)"
/* Support users (Operators) — Settings ▾ → Support. The back-office's own staff
   accounts: Affiliate(1) · Customer Care(4) · Administration(6). List
   (supportindex + getSupportUsersList), the three creation modals
   (newCustomerCareForm / newAmministrazioneForm / newAffiliateForm →
   saveNewCustomerCare / saveNewAmministrazione / saveNewAffiliate), the per-row
   editor (showSupportUserDetails, POST /saveUser/{id}/home) and the permission
   tree (showSupportUserPermissions, POST /saveUser/{id}/permissions →
   updateUserPermissions).

   Fidelity notes (cites are UsersController.php unless said otherwise):
   - NO KPI strip, NO bulk actions, NO global search box. The real screen has
     none: the DataTables `f` search input renders but the server only reads
     per-column searches, so it does nothing (ref "Filters"). Not reproduced —
     a control that silently does nothing is worse than no control.
   - The controller also implements `email`, `data_creazione` and `parent_id`
     column filters (:1514-1555) that this screen defines no columns for — dead
     branches. Not surfaced as filters here.
   - Sortable set is honest: the server-side switch handles only `id`,
     `username` and `last_login` (:1578-1605); everything else silently falls
     back to `users.id ASC`. Only those three columns are sortable here.
   - Lastname and Name(firstname) are hidden by default exactly as the real
     columnDefs do (supportajax.js:130-145), and reachable through the Columns
     control. Skin renders only for viewers with user_level < ADMIN_LEVEL
     (supportindex :133-135) — the demo session is Super admin(0), so it shows.
   - Header buttons follow the real conditions: New Customer Care always; New
     Administration only when user_level < ADMIN_LEVEL; New Affiliate only for
     isadmin(). Row Delete renders only when the client `user.is_admin == 1`.
   - Permission tree content comes from the Batch 6 back-office permission
     catalogue (User::getNestedUserPermissions, app/Models/User.php:532-638) —
     every key below is a real `checkUserBoPerm` string with its real call
     sites. Nothing invented; keys that are checked but NOT assignable
     (support_report_business, support_transfer, support_sport_coupons_payout,
     support_sport_can_reload_coupons, support_sport_jackpots,
     support_sport_coupons_changestatus, support_commission_profiles) are named
     in a note, NOT added to the tree.

   KNOWN BUGS — implemented as evident intent per the build policy, each with a
   SUGGESTION naming the fix:
   1. `saveNewAmministrazione` does not exist anywhere in the codebase; only the
      route (routes/admin.php:631-633) and the form action
      (newAmministrazione.blade.php:8) reference it, so submitting the New
      Administration modal 500s with BadMethodCallException. Here the create
      flow works.
      <!-- SUGGESTION: add UsersController::saveNewAmministrazione — mirror
           saveNewCustomerCare (:1802-1917) with user_level = ADMINISTRATION(6)
           and the single-skin_id resolution newAmministrazione.blade.php posts.
           Until it exists, POST /saveNewAmministrazione is a guaranteed 500. -->
   2. `last_login` single-bound filter: with only a from- or to-date the query
      references un-joined aliases `last_login.last_login` / `.addedTime`
      (:1565-1572) → SQL error; only the both-bounds range works. Here either
      bound alone filters correctly.
      <!-- SUGGESTION: fix the single-bound branches at UsersController:1565-1572
           to reference users.last_login like the both-bounds branch does. -->
   3. Export covers the current page only (DataTables PDF button, client-side,
      visible columns — supportajax.js:22-86). Here it exports the whole
      filtered result set.
      <!-- SUGGESTION: back the Support-users export with a server route over
           the filtered query instead of the client-side current-page PDF. -->
   4. Home-tab save silently unblocks: saveEditUser always recomputes
      `blocked`/`cash_block` from checkbox presence (:3981-3982) but the support
      Home form renders no such checkboxes, so a plain save writes 0. Here
      saving Home leaves the block flags untouched.
      <!-- SUGGESTION: in saveEditUser's home case, only write blocked /
           cash_block when the request actually carries those inputs. -->
   5. The declared `lengthMenu [5,10,25,50]` is inert because dom 'Bfrtip' has
      no length selector (supportajax.js:90). The selector is wired here.
      <!-- SUGGESTION: add 'l' to the DataTables dom string so the declared
           lengthMenu is reachable. -->
   6. `support_withdraws` reuses `'ref' => 'support_deposits'` (User.php:608),
      producing two checkboxes with id="perm_support_deposits" and confusing the
      toggler JS. The tree here keys both rows correctly and flags the collision.
      <!-- SUGGESTION: set the support_withdraws entry's ref to
           'support_withdraws' in User::getNestedUserPermissions. -->
   Faithfully NOT fixed (they are absences, not bugs): the Providers tab for
   Affiliate rows 404s because its route is commented out (routes/admin.php:600-
   602) — rendered here as a disabled tab, not a working screen; and the
   permission tree is genuinely empty for Affiliate/Administration rows because
   getNestedUserPermissions only implements the CUSTOMER_CARE case.

   Label policy: almost every string on this screen resolves only to a raw
   backend.* / commissions.* key in the committed default lang (runtime
   storage/lang is gitignored) — all backend.cc_permission_*, show_players,
   show_users, show_sport_coupons, show_commissions, sport_settings, deposits,
   withdraws, insert_password, copy_credentials, fill_all_fields,
   pass_not_match, and commissions.pay_commissions / edit_payments. Operator-
   facing English is written here and marked "label inferred". The real detail
   Info box hardcodes Italian ("Ultimo accesso", "Data registrazione", "Mai");
   English is used, same policy.

   Demo session = Super admin (level 0): all three create buttons, the Skin
   column + filter, the row Delete action, impersonate, and the isadmin()-only
   Settings branch of the permission tree are all visible. Mock rows are
   deterministic (seeded PRNG) so the list renders identically each load. */

const { useState: useStateHsu2, useMemo: useMemoHsu2, useEffect: useEffectHsu2 } = React;

/* ---------------- helpers ---------------- */
const hsu2Toast = (m, isErr) => window.PAYBO && window.PAYBO.emitToast && window.PAYBO.emitToast({
  id: `hsu2-${Date.now()}-${Math.floor(Math.random() * 1e5)}`, tx_id: m, amount: 0,
  currency: isErr ? "ERR" : "HOST", player: "Support users",
  reason: isErr ? "Fix this before continuing." : "Prototype state only \u2014 not persisted.",
});

/* Deterministic PRNG (mulberry32) — mock rows render identically each load. */
const hsu2Rng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
const hsu2Norm = (s) => String(s == null ? "" : s).trim().toLowerCase();
/* CSV writer — delegates to the shared hrsCsv from src/report-shell.jsx. */
const hsu2Csv = (rows, headers, filename) => window.hrsCsv(rows, headers, filename);
/* date("d/m/Y G:i") — day/month zero-padded, hour NOT padded, like the real list. */
const hsu2Date = (ts) => {
  if (!ts) return "-";
  const d = new Date(ts), p = (n) => String(n).padStart(2, "0");
  return `${p(d.getDate())}/${p(d.getMonth() + 1)}/${d.getFullYear()} ${d.getHours()}:${p(d.getMinutes())}`;
};
const hsu2IsoDay = (ts) => { const d = new Date(ts), p = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; };
// validateUsername()'s charset lives server-side and is not committed —
// alphanumeric + . _ - inferred from observed usernames ("label inferred").
const hsu2ValidUser = (u) => /^[A-Za-z0-9._-]+$/.test(u);

const useHsu2Mobile = () => {
  const [m, setM] = useStateHsu2(() => typeof window !== "undefined" && window.innerWidth <= 860);
  useEffectHsu2(() => {
    const on = () => setM(window.innerWidth <= 860);
    window.addEventListener("resize", on);
    return () => window.removeEventListener("resize", on);
  }, []);
  return m;
};

/* ---------------- enums ---------------- */
/* users.user_level is restricted to whereIn(1, 4, 6) on this screen (:1609);
   non-super-admin viewers additionally lose AFFILIATE (:1611-1613).
   Labels from usersLevels() (:1188-1217) — these three DO resolve in the
   committed default lang (backend.usertype_affiliate / _customer_care /
   _administration, public/default-lang/en/backend.php:591-593). */
const HSU2_ROLES = [
  { lvl: 4, name: "Customer Care",  color: "#28387a" },
  { lvl: 6, name: "Administration", color: "#0aa19a" },
  { lvl: 1, name: "Affiliate",      color: "#4b1fb3" },
];
const hsu2Role = (lvl) => HSU2_ROLES.find(r => r.lvl === lvl) || { lvl, name: `Level ${lvl}`, color: "#7a8194" };

const HSU2_SKINS = ["AcarayBets", "Casino24hs", "GoldenSky", "Tucasino", "Juegojoker", "Play365Vivo", "Jugaygana", "win24hs"];
const HSU2_TZ = ["(GMT-3:00) America/Buenos_Aires", "(GMT-4:00) America/Santiago", "(GMT-4:00) America/Asuncion", "(GMT-3:00) America/Sao_Paulo", "(GMT+1:00) Europe/Rome"];

/* Demo session. isadmin() → SUPER_ADMIN(0): sees the Skin column, all three
   create buttons, the Delete action and the isadmin()-only Settings branch. */
const HSU2_ME = { id: 1, username: "iwadmin", lvl: 0, roleName: "Super admin", isAdmin: true, skin: "Play365Vivo" };

/* ==================================================================
   PERMISSION CATALOGUE
   Every key below is a real checkUserBoPerm string. Labels + lang keys
   from User::getNestedUserPermissions (app/Models/User.php:562-616);
   `gates` text from the Batch 6 catalogue's "What it gates" column.
   ================================================================== */
const HSU2_PERMS = {
  support_dashboard: { label: "Dashboard", src: "backend.dashboard", gates: "Dashboard page and sidebar entry, plus the topbar balance/credits boxes. A scoped manager without it is redirected to /players on login." },

  support_players: { label: "Players", src: "backend.show_players", inferred: true, sticky: true, gates: "Players list page + its data endpoint, the KYC verifications screen, the view/update-player policy, the Players sidebar entry and the players block on the dashboard." },
  support_player_personal_data: { label: "Player personal data", src: "backend.cc_permission_player_personal_data", inferred: true, gates: "Editing a player's personal data; the name / email / IP columns and their search inputs on the players list; unmasking personal fields in the player form." },
  support_player_transactions_read_only: { label: "Player transfers", src: "backend.cc_permission_player_transfer", inferred: true, gates: "The Transfer page and executing transfers TO a player, the Transfer sidebar entry, the player Deposit tab and the \"player\" option in the transfer type selector." },
  support_player_transactions: { label: "Player transactions", src: "backend.cc_permission_player_transactions", inferred: true, gates: "The player History tab and its transaction data endpoint, plus the History and Stats tab links." },
  support_player_promotions: { label: "Player promotions", src: "backend.promotions", gates: "The player Promotions tab, its data endpoint, per-player bonus-promotion actions, the promotions export and the tab link." },
  support_player_export: { label: "Export players", src: "backend.export", gates: "The export button on the players list — and, reusing this same player key, the export button on the USERS list too." },

  support_users: { label: "Users", src: "backend.show_users", inferred: true, sticky: true, gates: "Users list page + its data endpoint, the user transactions page, the view-user policy, the Users sidebar entry and the users block on the dashboard." },
  support_user_personal_data: { label: "User personal data", src: "backend.cc_permission_user_personal_data", inferred: true, gates: "Editing a non-player user's personal data; masking/disabling the personal-data and password fields in the user form. Without it saveEditUser discards every personal field server-side." },
  support_user_transactions_read_only: { label: "User transfers", src: "backend.cc_permission_user_transfer", inferred: true, gates: "The Transfer page and agent-side transfers, the Transfer sidebar entry, the user Deposit tab link and the \"agent\" option in the transfer type selector." },
  support_user_permissions: { label: "User permissions tab", src: "backend.cc_permission_user_permissions", inferred: true, gates: "The Permissions tab of a user's editor — page and tab link." },
  support_user_transactions: { label: "User transactions", src: "backend.transactions", gates: "The user Transactions tab page and its data endpoint." },
  support_user_credit_transactions: { label: "User credit transactions", src: "backend.credit_transactions", gates: "The user Credit Transactions tab page and its data endpoint." },

  support_sport_bet: { label: "Bet from back office", src: "backend.bet", inferred: true, gates: "The bet-from-backoffice page (403 without it) and its sidebar entry." },

  support_cms_bonus_payments: { label: "Bonus payments", src: "backend.bonus_payments", inferred: true, gates: "The bonus-payments viewAny / view / pay policy. Its sidebar branch is an @elseif with an empty body — dead." },
  support_cms_banners: { label: "Banners", src: "backend.banners", inferred: true, gates: "The banners / slideshows index (404 without it), the per-skin banner create/edit guard, and the CMS sidebar menu." },

  support_skin_providers: { label: "Skin providers", src: "backend.providers", inferred: true, gates: "The skin-management sidebar group, the per-skin Providers links, and the skin Providers screen + tab endpoints." },
  support_skin_gamemanagement: { label: "Skin game management", src: "backend.game_management", inferred: true, gates: "The per-skin Game Management links and every game-management endpoint of SkinsController, plus the skin tab link." },
  support_skin_subcategories: { label: "Skin subcategories", src: "backend.subcategories", inferred: true, gates: "The per-skin Subcategories links and every subcategory-management endpoint, plus the skin tab link." },

  support_disable_transfers: { label: "Disable transfers (Deposit & Withdraw)", src: "hardcoded string", deny: true, gates: "INVERTED. Granting this REMOVES access: 403 on the transfer page, a hard block inside dotransfer, and the Transfer sidebar entry disappears." },

  support_report: { label: "Reports", src: "backend.report (plural)", master: true, sticky: false, gates: "Master switch. It is AND-ed with the specific report permission on EVERY report page and data endpoint — without it every child below is inert. Also part of the view-user policy and gates the Reports sidebar group." },
  support_report_network_liabilities: { label: "Network liabilities report", src: "backend.report_network_liabilities", inferred: true, gates: "Network Liabilities report page, data endpoint and sidebar entry." },
  support_report_players: { label: "Players report", src: "backend.report_players", inferred: true, gates: "Players report page, data endpoint and sidebar entry." },
  support_report_betting: { label: "Bet report", src: "backend.report_betting", inferred: true, gates: "Bet report page, data endpoint, sub-level drill-down and sidebar entry." },
  support_report_daily_report: { label: "Daily report", src: "backend.report_daily", inferred: true, gates: "Daily report page and data — and ALSO the per-day / sub-row drill-downs inside the Players, Network Liabilities, Bet, Bet Type, Summary and Credit Transactions reports." },
  support_report_transactions: { label: "Transactions report", src: "backend.report_transactions", inferred: true, gates: "Transactions report page, data endpoint and sidebar entry." },
  support_report_credit_transactions: { label: "Credit transactions report", src: "backend.report_credit_transactions", inferred: true, gates: "Credit Transactions report page, data endpoint and sidebar entry." },
  support_report_commissions: { label: "Commissions report", src: "backend.report_commissions", inferred: true, gates: "Commissions report page, data endpoint and sidebar entry." },
  support_report_summary: { label: "Summary report", src: "backend.report_summary", inferred: true, gates: "Summary report page, its three query branches and its sidebar entry." },
  support_report_netwin: { label: "NetWin", src: "hardcoded string", gates: "NetWin report page, data endpoint and sidebar entry." },
  support_report_dailyperformance: { label: "Daily Performance", src: "hardcoded string", gates: "Daily Performance report page, data endpoint and sidebar entry." },
  support_report_bet_type: { label: "Bet Type report", src: "backend.report_bet_type", inferred: true, gates: "Bet Type report page, data endpoint and sidebar entry." },
  support_export: { label: "Export reports", src: "backend.export", gates: "The Export button on every report screen. View-only gate — no server-side re-check was found, so a direct request to an export endpoint is not blocked by it." },

  support_deposits: { label: "Deposits", src: "backend.deposits", inferred: true, gates: "The Deposits sidebar entry (with its pending-deposits counter) — and nothing else. No controller re-checks it." },
  support_withdraws: { label: "Withdraws", src: "backend.withdraws", inferred: true, dup: true, gates: "The Withdraws sidebar entry (with its pending counter) — and nothing else. No controller re-checks it." },

  support_commission: { label: "Commissions", src: "backend.show_commissions", inferred: true, sticky: true, gates: "The commission-payments viewAny policy and the \"Commissions payments\" sidebar entry." },
  support_commission_payments: { label: "Pay commissions", src: "commissions.pay_commissions", inferred: true, gates: "Paying an unpaid commission payment (the pay policy)." },
  support_commission_edit_payments: { label: "Edit payments", src: "commissions.edit_payments", inferred: true, gates: "Editing an unpaid commission payment (the update policy)." },

  support_settings: { label: "Sport", src: "backend.sport", never: true, gates: "Never checked anywhere — only its child support_settings_sport is. Assignable but inert on its own." },
  support_settings_sport: { label: "Sport settings", src: "backend.sport_settings", inferred: true, gates: "The sport payout-settings page (view + save) and the payout flag in the sidebar." },

  support_sport_coupons: { label: "Sport coupons", src: "backend.show_sport_coupons", inferred: true, sticky: true, gates: "Sport coupons list + data, the player/user coupon-history tabs and their links, the coupon view policy, and the Sport Coupons sidebar entry." },
  support_sport_coupons_export: { label: "Export coupons", src: "backend.export", gates: "The Export button on the sport coupons list. View-only gate." },
};

/* Sections = how the tree is drawn. Real structure is three-level: group header
   (key === false, not assignable) → parent permission → children. Five keys
   render with NO group header at all (User.php:562, 582, 592, 607, 608); they
   are collected into the first card purely so the tree scans — that card's
   title is the only one this file invents, and it says so. */
const HSU2_TREE = [
  {
    id: "top", title: "Ungrouped permissions", inferredTitle: true,
    note: "These five keys render without a group header on the real screen (User.php:562, 582, 592, 607, 608). Boxed together here for scanability only — the grouping is this prototype's, the keys are not.",
    nodes: [
      { key: "support_dashboard" },
      { key: "support_sport_bet" },
      { key: "support_disable_transfers" },
      { key: "support_deposits" },
      { key: "support_withdraws" },
    ],
  },
  {
    id: "players", title: "Players", note: "Group header at User.php:564 — the header itself carries no key and is not assignable.",
    nodes: [{ key: "support_players", children: ["support_player_personal_data", "support_player_transactions_read_only", "support_player_transactions", "support_player_promotions", "support_player_export"] }],
  },
  {
    id: "users", title: "Users", note: "Group header at User.php:573.",
    nodes: [{ key: "support_users", children: ["support_user_personal_data", "support_user_transactions_read_only", "support_user_permissions", "support_user_transactions", "support_user_credit_transactions"] }],
  },
  {
    id: "cms", title: "CMS", note: "Group header at User.php:584 — two direct keys, no parent permission between them.",
    nodes: [{ key: "support_cms_bonus_payments" }, { key: "support_cms_banners" }],
  },
  {
    id: "skin", title: "Skin management", note: "Group header at User.php:588 (backend.skin_management) — three direct keys, no parent permission.",
    nodes: [{ key: "support_skin_providers" }, { key: "support_skin_gamemanagement" }, { key: "support_skin_subcategories" }],
  },
  {
    id: "reports", title: "Reports", note: "support_report is a top-level key with twelve children and no group header of its own (User.php:593).",
    nodes: [{
      key: "support_report", children: ["support_report_network_liabilities", "support_report_players", "support_report_betting", "support_report_daily_report", "support_report_transactions", "support_report_credit_transactions", "support_report_commissions", "support_report_summary", "support_report_netwin", "support_report_dailyperformance", "support_report_bet_type", "support_export"],
    }],
  },
  {
    id: "commissions", title: "Commissions", note: "Group header at User.php:610. A third child, support_commission_profiles, is commented out on both the assignment and the check side — dead, so it is not offered.",
    nodes: [{ key: "support_commission", children: ["support_commission_payments", "support_commission_edit_payments"] }],
  },
  {
    id: "settings", title: "Settings", adminOnly: true, note: "Appended only when the VIEWER is isadmin() (User.php:619-621). The demo session is Super admin, so it shows.",
    nodes: [{ key: "support_settings", children: ["support_settings_sport"] }],
  },
  {
    id: "coupons", title: "Sport coupons", note: "Appended at User.php:623-627 — the isadmin()/enable_payout_tickets branch picks between two identical trees, so every viewer gets the same two keys.",
    nodes: [{ key: "support_sport_coupons", children: ["support_sport_coupons_export"] }],
  },
];

/* Parents exempt from the toggler's auto-uncheck (supportpermissions.blade.php:186). */
const HSU2_STICKY = ["support_commission", "support_players", "support_users", "support_sport_coupons"];

/* Checked in code but assignable NOWHERE — a Customer Care account can only get
   these through a hand-written row in the `permissions` table. Listed, not offered. */
const HSU2_UNASSIGNABLE = [
  ["support_report_business", "Business report page + data — the Business report is fully locked out for Customer Care."],
  ["support_transfer", "Inverted in the controller (having it kills the transfer page) but enabling in the sidebar — contradictory and unreachable."],
  ["support_sport_coupons_payout", "Coupon payout controls across the layout and the coupons table."],
  ["support_sport_can_reload_coupons", "The reload-coupon action on the coupons table."],
  ["support_sport_jackpots", "An empty @elseif branch in the sidebar — dead."],
  ["support_sport_coupons_changestatus", "Its only check sits inside a commented block; the tree removal that references it is a no-op."],
  ["support_commission_profiles", "All four check sites and both assignment lines are commented out — dead on both sides."],
];

const hsu2FlatKeys = (adminViewer) => {
  const out = [];
  HSU2_TREE.forEach(s => { if (s.adminOnly && !adminViewer) return; s.nodes.forEach(n => { out.push(n.key); (n.children || []).forEach(c => out.push(c)); }); });
  return out;
};

/* ==================================================================
   MOCK ROWS
   ================================================================== */
/* ---------- the row source ------------------------------------------------
   Was 34 invented staff accounts — six hand-placed to cover every documented
   shape, 28 more from a seeded RNG. Now `users`, live.

   Support users are not an entity: isystem has no separate table and neither do
   we. The screen is a FILTER over `users` on the staff levels — Affiliate (1),
   Customer Care (4) and Administration (6). RLS still scopes the result to the
   caller's own subtree, so an operator sees their own staff and nobody else's.

   `skins` is an array on the real screen because isystem lets a Customer Care
   account span several via multiple_skins. Our `users.skin_id` is a single
   tenant — the multi-skin case has no representation yet, so this renders the
   one skin rather than faking a list.
   <!-- SUGGESTION: model multi-skin staff as user_skins(user_id, skin_id) if
        cross-tenant support accounts are actually wanted. --> */
const HSU2_STAFF_LEVELS = [1, 4, 6];

const hsu2Row = (u) => ({
  id: u.id,
  username: u.username,
  firstname: u.firstname,
  lastname: u.lastname,
  lvl: u.user_level,
  skins: u.skin_id == null ? [] : [String(u.skin_id)],
  parent: null,           // parent_id is not selected on this resource
  last: u.last_login_at ? Date.parse(u.last_login_at) : 0,
  reg: u.created_at ? Date.parse(u.created_at) : 0,
  email: u.email,
  blocked: !!u.blocked,
});

/* WAS `hsu2SeedGrants`: a seeded PRNG that ticked roughly 45% of the tree.
   Deterministic, so it looked stable across reloads, and completely invented —
   an operator opening this tab was shown a plausible set of permissions that
   corresponded to nothing at all. Permissions are the one thing on this screen
   where a confident wrong answer is worse than an empty one.

   Real grants now come from `user_permissions` (030) through
   `Hsu2Enforced` below, and writes go through `set_user_permission` (040).
   Nothing here generates a grant. */

/* ==================================================================
   small presentational pieces
   ================================================================== */
const Hsu2Role = ({ lvl }) => {
  const r = hsu2Role(lvl);
  return <span className="hsu2-rolechip" style={{ background: r.color }}>{r.name}<i>{r.lvl}</i></span>;
};

const Hsu2Key = ({ children }) => <code className="hsu2-key">{children}</code>;

const Hsu2Modal = ({ title, sub, onClose, children, footer, wide }) => {
  const mobile = useHsu2Mobile();
  useEffectHsu2(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="hsu2-scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className={`hsu2-modal${wide ? " hsu2-modal--wide" : ""}${mobile ? " hsu2-modal--full" : ""}`} role="dialog" aria-modal="true">
        <div className="hsu2-modal__head">
          <div>
            <div className="hsu2-modal__title">{title}</div>
            {sub && <div className="hsu2-modal__sub">{sub}</div>}
          </div>
          <button className="hsu2-modal__x" onClick={onClose} title="Close"><Icon name="x" size={16} /></button>
        </div>
        <div className="hsu2-modal__body">{children}</div>
        {footer && <div className="hsu2-modal__foot">{footer}</div>}
      </div>
    </div>
  );
};

const Hsu2Field = ({ label, req, err, note, children, span2 }) => (
  <div className={`hsu2-field${span2 ? " hsu2-span2" : ""}${err ? " hsu2-field--err" : ""}`}>
    <label className="form-label">{label}{req && " *"}</label>
    {children}
    {err && <div className="hsu2-field__err">{err}</div>}
    {note && !err && <div className="hsu2-field__note">{note}</div>}
  </div>
);

const Hsu2Callout = ({ tone = "warn", icon = "alert", title, children }) => (
  <div className={`hsu2-callout hsu2-callout--${tone}`}>
    <Icon name={icon} size={14} />
    <div>{title && <b>{title}</b>}{title && " "}{children}</div>
  </div>
);

/* Skin multi-select. Customer Care: multi when isAdmin(), otherwise a hidden
   input pinned to the creator's own skin. Administration: single select.
   Affiliate: always multi. Server rejects an empty selection with
   "Assign at least one skin" (:1818-1821, :2220-2224). */
const Hsu2SkinPicker = ({ multi, value, onChange, disabled }) => {
  const [q, setQ] = useStateHsu2("");
  const shown = HSU2_SKINS.filter(s => !q || hsu2Norm(s).includes(hsu2Norm(q)));
  if (!multi) {
    return (
      <select className="select" value={value[0] || ""} disabled={disabled} onChange={e => onChange(e.target.value ? [e.target.value] : [])}>
        <option value="">- Select -</option>
        {HSU2_SKINS.map(s => <option key={s} value={s}>{s}</option>)}
      </select>
    );
  }
  const toggle = (s) => onChange(value.includes(s) ? value.filter(x => x !== s) : value.concat(s));
  return (
    <div className="hsu2-skins">
      <input className="input hsu2-skins__q" placeholder="Filter skins…" value={q} onChange={e => setQ(e.target.value)} disabled={disabled} />
      <div className="hsu2-skins__list">
        {shown.map(s => (
          <label key={s} className={`hsu2-skins__opt${value.includes(s) ? " is-on" : ""}`}>
            <input type="checkbox" checked={value.includes(s)} disabled={disabled} onChange={() => toggle(s)} />
            <span>{s}</span>
          </label>
        ))}
        {shown.length === 0 && <div className="hsu2-skins__none">No skin matches “{q}”.</div>}
      </div>
      <div className="hsu2-skins__foot">
        {value.length === 0 ? "No skin selected — the save is rejected with “Assign at least one skin”." : `${value.length} selected · stored as multiple_skins`}
      </div>
    </div>
  );
};

/* Shared personal-data block — userPersonalData() (app/Helpers/user_forms.php:360-808).
   Every field here is OPTIONAL: config/user_fields.php lists only
   ["username","password"], and userPostFields() validates a personal field only
   if it appears in that list. Italy swaps province/city to selects and adds
   fiscal_code (validated with the CodiceFiscale class when non-empty). */
const Hsu2Personal = ({ v, set, errs, masked }) => {
  const COUNTRIES = ["Argentina", "Bolivia", "Brazil", "Chile", "Italy", "Paraguay", "Spain"];
  const IT_PROV = ["Milano", "Roma", "Napoli", "Torino"];
  const years = Array.from({ length: 60 }, (_, i) => 2008 - i);
  const F = (k) => ({ value: v[k] || "", disabled: masked, onChange: (e) => set(k, e.target.value) });
  return (
    <>
      <div className="hsu2-formsec">
        Personal data
        <span className="hsu2-formsec__opt">all optional — config/user_fields.php requires only username + password</span>
      </div>
      {masked && (
        <Hsu2Callout tone="warn" icon="lock" title="Masked.">
          This viewer is a Customer Care account without <Hsu2Key>support_user_personal_data</Hsu2Key>, so
          maskPersonalDataIfNotAllowed() disables and masks these inputs — and saveEditUser discards every
          personal field server-side even if they were forced through.
        </Hsu2Callout>
      )}
      <div className="hsu2-form3">
        <Hsu2Field label="Name" err={errs.firstname}><input className="input" {...F("firstname")} /></Hsu2Field>
        <Hsu2Field label="Lastname"><input className="input" {...F("lastname")} /></Hsu2Field>
        <Hsu2Field label="Gender">
          <select className="select" {...F("sex")}><option value="">Select</option><option value="m">Male</option><option value="f">Female</option></select>
        </Hsu2Field>
        <Hsu2Field label="Country of birth">
          <select className="select" {...F("country_birth")}><option value="">- Select -</option>{COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}</select>
        </Hsu2Field>
        {v.country_birth === "Italy" ? (
          <>
            <Hsu2Field label="Province of birth"><select className="select" {...F("province_birth")}><option value="">- Select -</option>{IT_PROV.map(p => <option key={p} value={p}>{p}</option>)}</select></Hsu2Field>
            <Hsu2Field label="City of birth" note="Loaded by AJAX from the chosen province"><select className="select" {...F("city_birth")}><option value="">- Select -</option>{IT_PROV.map(p => <option key={p} value={p}>{p}</option>)}</select></Hsu2Field>
          </>
        ) : (
          <>
            <Hsu2Field label="Province of birth"><input className="input" {...F("province_text_birth")} /></Hsu2Field>
            <Hsu2Field label="City of birth"><input className="input" {...F("city_text_birth")} /></Hsu2Field>
          </>
        )}
        <Hsu2Field label="Birthday" note="Year list runs from current−18 down to 1940">
          <div className="hsu2-inline">
            <select className="select" {...F("birth_day")}><option value="">D</option>{Array.from({ length: 31 }, (_, i) => i + 1).map(d => <option key={d}>{d}</option>)}</select>
            <select className="select" {...F("birth_month")}><option value="">M</option>{Array.from({ length: 12 }, (_, i) => i + 1).map(m => <option key={m}>{m}</option>)}</select>
            <select className="select" {...F("birth_year")}><option value="">Year</option>{years.map(y => <option key={y}>{y}</option>)}</select>
          </div>
        </Hsu2Field>
        <Hsu2Field label="Email" err={errs.email} note="If filled, the account is created with email_confirmed = true">
          <input className="input" type="email" placeholder="name@mail.com" {...F("email")} />
        </Hsu2Field>
        <Hsu2Field label="Mobile phone" note="If filled, mobile_verified is set to true">
          <div className="hsu2-inline">
            <select className="select hsu2-prefix" {...F("mobile_prefix")}><option value="">+…</option><option>+54</option><option>+55</option><option>+56</option><option>+591</option><option>+595</option><option>+39</option></select>
            <input className="input" {...F("mobile")} />
          </div>
        </Hsu2Field>
      </div>

      <div className="hsu2-formsec">Residence &amp; documents</div>
      <div className="hsu2-form3">
        <Hsu2Field label="Address" span2>
          <div className="hsu2-inline">
            <input className="input" placeholder="Street" {...F("address_residence")} />
            <input className="input hsu2-housenr" placeholder="No." {...F("address_house_number")} />
          </div>
        </Hsu2Field>
        <Hsu2Field label="Zip"><input className="input" {...F("zip_residence")} /></Hsu2Field>
        <Hsu2Field label="Country of residence">
          <select className="select" {...F("country_residence")}><option value="">- Select -</option>{COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}</select>
        </Hsu2Field>
        {v.country_residence === "Italy" ? (
          <>
            <Hsu2Field label="Province"><select className="select" {...F("province_residence")}><option value="">- Select -</option>{IT_PROV.map(p => <option key={p} value={p}>{p}</option>)}</select></Hsu2Field>
            <Hsu2Field label="City"><select className="select" {...F("city_residence")}><option value="">- Select -</option>{IT_PROV.map(p => <option key={p} value={p}>{p}</option>)}</select></Hsu2Field>
            <Hsu2Field label="Fiscal code" note="Italian residence only — “Calculate” posts to /calcCodFisc; a non-empty value is validated with the CodiceFiscale class">
              <div className="hsu2-inline">
                <input className="input" {...F("fiscal_code")} />
                <button className="rpt-btn rpt-btn--reset hsu2-calc" disabled={masked} onClick={() => hsu2Toast("Calculate fiscal code — POST /calcCodFisc")}>Calculate</button>
              </div>
            </Hsu2Field>
          </>
        ) : (
          <>
            <Hsu2Field label="Province"><input className="input" {...F("province_text_residence")} /></Hsu2Field>
            <Hsu2Field label="City"><input className="input" {...F("city_text_residence")} /></Hsu2Field>
          </>
        )}
        <Hsu2Field label="Document type">
          <select className="select" {...F("document_type")}><option value="">Select</option><option>ID card</option><option>Passport</option><option>Driving licence</option></select>
        </Hsu2Field>
        <Hsu2Field label="Document number" note="Read-only on edit once set, unless the viewer is admin or skin admin">
          <input className="input" {...F("document_number")} />
        </Hsu2Field>
      </div>
    </>
  );
};

/* ==================================================================
   CREATE MODALS — newCustomerCareForm / newAmministrazioneForm /
   newAffiliateForm, saved by saveNewCustomerCare / saveNewAmministrazione
   (MISSING — see the header note) / saveNewAffiliate.
   ================================================================== */
const HSU2_CREATE_DEFS = {
  cc: {
    lvl: 4, title: "New Customer Care", form: "GET /newCustomerCareForm", save: "POST /saveNewCustomerCare",
    multiSkin: true, strictPwd: true,
    note: "Super admin picks any number of skins: exactly one also re-parents the account to that skin's admin (getSkinAdmin); two or more leave skin_id = 0 and parent_id = 1. A subnet creator or skin admin gets their own skin and becomes the parent.",
  },
  adm: {
    lvl: 6, title: "New Administration", form: "GET /newAmministrazioneForm", save: "POST /saveNewAmministrazione",
    multiSkin: false, strictPwd: true, broken: true,
    note: "Single skin select, rendered only for isAdmin(). The save target does not exist in the codebase — see the banner inside the modal.",
  },
  aff: {
    lvl: 1, title: "New Affiliate", form: "GET /newAffiliateForm", save: "POST /saveNewAffiliate",
    multiSkin: true, strictPwd: false,
    note: "Affiliates are always created at the root: parent_id = 1, skin_id = 0, and the selection is stored in multiple_skins.",
  },
};

const Hsu2CreateModal = ({ kind, existing, onCreate, onClose }) => {
  const def = HSU2_CREATE_DEFS[kind];
  const [v, setV] = useStateHsu2({});
  const [skins, setSkins] = useStateHsu2([]);
  const [errs, setErrs] = useStateHsu2({});
  const [showPwd, setShowPwd] = useStateHsu2(false);
  const set = (k, val) => setV(s => Object.assign({}, s, { [k]: val }));

  const validate = () => {
    const e = {};
    const u = (v.username || "").trim();
    if (!u) e.username = "Username is required.";
    else if (!hsu2ValidUser(u)) e.username = "Only letters, digits and . _ - are accepted.";
    else if (existing.some(r => hsu2Norm(r.username) === hsu2Norm(u))) e.username = "This username is already taken.";
    const p = v.password || "";
    if (!p) e.password = "Password is required.";
    else if (def.strictPwd) {
      if (p.length < 6) e.password = "Minimum 6 characters.";
      else if (p.length > 30) e.password = "Maximum 30 characters.";
      else if (p !== (v.password_confirmation || "")) e.password_confirmation = "The two passwords do not match.";
    }
    if (v.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v.email)) e.email = "Not a valid email address.";
    if (skins.length === 0) e.skins = "Assign at least one skin.";
    setErrs(e);
    return Object.keys(e).length === 0;
  };

  const submit = () => {
    if (!validate()) { hsu2Toast("Fill in all required fields", true); return; }
    onCreate({
      username: (v.username || "").trim(), firstname: v.firstname || "", lastname: v.lastname || "",
      lvl: def.lvl, skins: skins.slice(),
      parent: def.lvl === 1 ? "-" : (skins.length === 1 ? `${hsu2Norm(skins[0])}admin` : "-"),
      last: 0, reg: Date.now(),
    });
  };

  const copyCreds = () => {
    const txt = `${v.username || ""} / ${v.password || ""}`;
    try { navigator.clipboard && navigator.clipboard.writeText(txt); } catch (_e) { /* clipboard unavailable */ }
    hsu2Toast("Credentials copied to clipboard");
  };

  return (
    <Hsu2Modal
      wide
      title={def.title}
      sub={`${def.form} → ${def.save} · user_level = ${def.lvl}`}
      onClose={onClose}
      footer={<>
        <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={onClose}>Cancel</button>
        <button className="rpt-btn rpt-btn--blue hsu2-fbtn" onClick={submit}><Icon name="check" size={13} /> Create {hsu2Role(def.lvl).name}</button>
      </>}>

      {def.broken && (
        /* KNOWN BUG (implemented as evident intent per the build policy):
           UsersController::saveNewAmministrazione does not exist — only the route
           (routes/admin.php:631-633) and the form action reference it, so the real
           modal 500s with BadMethodCallException on submit. This prototype completes
           the create the way saveNewCustomerCare would, and says so. */
        <Hsu2Callout tone="err" icon="alert" title="Broken on the live platform.">
          <Hsu2Key>POST /saveNewAmministrazione</Hsu2Key> is routed and the form posts to it, but
          <b> UsersController::saveNewAmministrazione does not exist anywhere in the codebase</b> — submitting this
          modal on the real admin returns a 500 (BadMethodCallException). This prototype implements the evident
          intent instead: the account is created exactly like a Customer Care one, with user_level 6.
          {/* SUGGESTION: add UsersController::saveNewAmministrazione mirroring
              saveNewCustomerCare (:1802-1917) with ADMINISTRATION_LEVEL and the
              single-skin resolution the blade posts. */}
        </Hsu2Callout>
      )}

      <div className="hsu2-formsec">Login data<span className="hsu2-formsec__opt">the only two required fields</span></div>
      <div className="hsu2-form3">
        <Hsu2Field label="Username" req err={errs.username} note="Checked against validateUsername() and re-checked for duplicates inside the save">
          <input className="input" autoFocus value={v.username || ""} onChange={e => set("username", e.target.value)} />
        </Hsu2Field>
        <Hsu2Field label="Password" req err={errs.password}
          note={def.strictPwd ? "required · min 6 · max 30 · must match the confirmation" : "Affiliate create checks only that it is non-empty — no length, no confirmation"}>
          <div className="hp-pwd">
            <input className="input" type={showPwd ? "text" : "password"} value={v.password || ""} onChange={e => set("password", e.target.value)} />
            <button className="hp-eye" onClick={() => setShowPwd(s => !s)} title={showPwd ? "Hide" : "Show"}><Icon name="eye" size={14} /></button>
          </div>
        </Hsu2Field>
        {def.strictPwd ? (
          <Hsu2Field label="Confirm password" req err={errs.password_confirmation}>
            <input className="input" type={showPwd ? "text" : "password"} value={v.password_confirmation || ""} onChange={e => set("password_confirmation", e.target.value)} />
          </Hsu2Field>
        ) : (
          <Hsu2Field label="Confirm password" note="Not rendered by the real Affiliate form — its save accepts any non-empty string">
            <input className="input" disabled placeholder="not required for Affiliate" />
          </Hsu2Field>
        )}
      </div>
      {!def.strictPwd && (
        <Hsu2Callout tone="warn" icon="shield" title="Weaker than every other create form.">
          saveNewAffiliate validates the password with a non-empty check only (:2230-2233) — no
          <code> confirmed</code>, no <code>min:6</code>, no <code>max:30</code>. Reproduced faithfully rather than
          silently hardened, so the gap is visible.
          {/* SUGGESTION: apply the same Validator rules saveNewCustomerCare uses
              (confirmed|required|string|max:30|Password::min(6)) to saveNewAffiliate. */}
        </Hsu2Callout>
      )}
      <div className="hsu2-credrow">
        <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={copyCreds}><Icon name="copy" size={13} /> Copy credentials</button>
        <span>Client-side only, and offered on create just like the real form. {/* label inferred */}</span>
      </div>

      <div className="hsu2-formsec">Skin assignment<span className="hsu2-formsec__opt">{def.multiSkin ? "multiple_skins" : "single skin_id"}</span></div>
      <Hsu2Field label={def.multiSkin ? "Skins" : "Skin"} req err={errs.skins} note={def.note}>
        <Hsu2SkinPicker multi={def.multiSkin} value={skins} onChange={setSkins} />
      </Hsu2Field>

      <Hsu2Personal v={v} set={set} errs={errs} masked={false} />
    </Hsu2Modal>
  );
};

/* ==================================================================
   PERMISSION TREE — POST /saveUser/{id}/permissions → saveEditUser
   case "permissions" → updateUserPermissions (row diff: create for newly
   checked, delete for unchecked).
   ================================================================== */
const Hsu2PermRow = ({ pkey, granted, onToggle, child, dimmed, readOnly }) => {
  const p = HSU2_PERMS[pkey];
  const flags = [];
  if (p.deny) flags.push(["deny", "Inverted flag — granting it REMOVES access"]);
  if (p.master) flags.push(["master", "Master switch — AND-ed with every child below"]);
  if (p.never) flags.push(["never checked", "Assignable but no code ever checks it"]);
  if (p.dup) flags.push(["duplicate id", "Its tree entry reuses ref 'support_deposits' (User.php:608), so the real page renders two checkboxes with the same DOM id"]);
  if (p.sticky) flags.push(["sticky parent", "Exempt from the toggler's auto-uncheck — stays on when its last child is cleared"]);
  return (
    <label className={`hsu2-prow${child ? " hsu2-prow--child" : " hsu2-prow--parent"}${granted ? " is-on" : ""}${dimmed ? " is-dimmed" : ""}`}>
      <input type="checkbox" checked={!!granted} disabled={readOnly} onChange={() => onToggle(pkey)} />
      <span className="hsu2-prow__body">
        <span className="hsu2-prow__lab">
          {p.label}
          {p.inferred && <i className="hsu2-inf" title="Resolves only to a raw lang key in the committed default lang — operator-facing wording written here">{/* label inferred */}label inferred</i>}
          <Tip size={12}><b>{pkey}</b><br />{p.gates}<br /><span style={{ opacity: 0.72 }}>Lang source: {p.src}</span></Tip>
        </span>
        <span className="hsu2-prow__meta">
          <Hsu2Key>{pkey}</Hsu2Key>
          {flags.map(([f, t]) => <span key={f} className={`hsu2-flag hsu2-flag--${f.split(" ")[0]}`} title={t}>{f}</span>)}
        </span>
      </span>
    </label>
  );
};

/* ==================================================================
   THE PERMISSIONS THAT ACTUALLY GATE SOMETHING
   ==================================================================
   Two vocabularies meet on this screen and only one of them is enforced.

   `HSU2_TREE` below is a faithful transcription of isystem's permission keys,
   documented down to which page each one gates. It is worth keeping and it is
   NOT what this platform checks. `permission_keys` (030) holds a deliberately
   smaller set — every key in it is read by something here, and the nine keys
   upstream grants that gate nothing were left out on purpose, because a tick
   that changes no behaviour is a promise to an operator that is not kept.

   So the enforced set is rendered first, with working toggles, and the
   upstream tree is rendered after it as reference, read-only. Mapping one onto
   the other was considered and rejected: `support_report_netwin` and
   `reports.view` are not the same key, several upstream keys have no
   counterpart at all, and guessing a correspondence between two permission
   models is exactly the kind of invention that does the most damage.

   <!-- SUGGESTION: if the enforced set should grow to cover more of the
        upstream tree, add rows to `permission_keys` and a call site for each —
        in that order, so a key never exists before the thing it gates. --> */
/* Stable identities, module scope: a fresh {} or () => {} on every render
   would give the read-only tree a new prop each time and re-render it for no
   reason. Named with the file's hsu2/HSU2 prefix because in-browser Babel makes
   every top-level const a window global and load order silently wins ties. */
const HSU2_NO_GRANTS = Object.freeze({});
const hsu2NoOp = () => {};

const Hsu2Enforced = ({ row }) => {
  const feed = useHrsFetch(() => Promise.all([
    window.sb.list("permissionKeys", { limit: 200 }),
    window.sb.list("userPermissions", { limit: 200, filters: { user: row.id } }),
  ]).then(([keys, held]) => {
    const bad = [keys, held].find(r => !r.ok);
    if (bad) return bad;
    return { ok: true, data: { keys: keys.data, held: held.data }, meta: {}, source: "live" };
  }), [row.id]);

  const saver = useHrsSave(feed);

  return (
    <div className="hsu2-enforced">
      <HrsAsync state={feed} skeletonRows={4} skeletonCols={2}>
        {(d) => {
          const held = new Set((d.held || []).map(r => r.key));

          /* Only the keys that apply to THIS role. app_has_permission refuses a
             key outside `applies_to` and set_user_permission refuses to write
             one, so offering it here would be a control whose only possible
             outcome is an error message. */
          const mine = (d.keys || []).filter(k => (k.applies_to || []).includes(row.lvl));
          if (mine.length === 0) {
            return (
              <HrsEmpty>
                {`No enforced permission applies to ${hsu2Role(row.lvl).name} accounts. ` +
                 `The tree below is upstream's and is not checked here.`}
              </HrsEmpty>
            );
          }

          const groups = [];
          mine.forEach(k => {
            let g = groups.find(x => x.category === k.category);
            if (!g) { g = { category: k.category, keys: [] }; groups.push(g); }
            g.keys.push(k);
          });

          const toggle = (k) => saver.run(
            () => window.sb.setUserPermission({
              userId: row.id, key: k.key, granted: !held.has(k.key),
            }),
            { done: held.has(k.key) ? "Permission revoked" : "Permission granted",
              fail: "Permission unchanged" });

          return (
            <div>
              {groups.map(g => (
                <div className="hsu2-enfgroup" key={g.category}>
                  <div className="hsu2-enfgroup__title">{g.category}</div>
                  {g.keys.map(k => (
                    <label className="hsu2-enfrow" key={k.key}>
                      <input type="checkbox" checked={held.has(k.key)} disabled={saver.busy}
                             onChange={() => toggle(k)} />
                      <span className="hsu2-enfrow__label">{k.label}</span>
                      <code className="hsu2-enfrow__key">{k.key}</code>
                      {k.note && <span className="hsu2-enfrow__note">{k.note}</span>}
                    </label>
                  ))}
                </div>
              ))}
            </div>
          );
        }}
      </HrsAsync>
    </div>
  );
};

const Hsu2PermTree = ({ row, grants, setGrants, adminViewer, readOnly }) => {
  const [q, setQ] = useStateHsu2("");
  const [collapsed, setCollapsed] = useStateHsu2({});
  const sections = HSU2_TREE.filter(s => !s.adminOnly || adminViewer);

  /* Toggler semantics, from supportpermissions.blade.php:186 —
     · checking a child auto-checks its parent;
     · clearing the last checked child auto-clears the parent, EXCEPT for
       support_commission / support_players / support_users / support_sport_coupons;
     · clearing a parent clears its children (the parent gates them, and an
       unposted checkbox is deleted by updateUserPermissions anyway). */
  const toggle = (key) => {
    setGrants(prev => {
      const next = Object.assign({}, prev);
      const parentNode = (() => {
        for (const s of sections) for (const n of s.nodes) if ((n.children || []).includes(key)) return n;
        return null;
      })();
      const self = sections.reduce((acc, s) => acc || s.nodes.find(n => n.key === key), null);

      if (next[key]) {
        delete next[key];
        if (self && self.children) self.children.forEach(c => delete next[c]);
        if (parentNode && !HSU2_STICKY.includes(parentNode.key) && !(parentNode.children || []).some(c => next[c])) delete next[parentNode.key];
      } else {
        next[key] = true;
        if (parentNode) next[parentNode.key] = true;
      }
      return next;
    });
  };

  const setMany = (keys, on) => setGrants(prev => {
    const next = Object.assign({}, prev);
    keys.forEach(k => { if (on) next[k] = true; else delete next[k]; });
    return next;
  });

  const matches = (key) => {
    if (!q) return true;
    const p = HSU2_PERMS[key], n = hsu2Norm(q);
    return hsu2Norm(p.label).includes(n) || hsu2Norm(key).includes(n) || hsu2Norm(p.gates).includes(n);
  };

  const visibleSections = sections.map(s => {
    const nodes = s.nodes.map(n => {
      const kids = (n.children || []).filter(matches);
      const selfHit = matches(n.key) || hsu2Norm(s.title).includes(hsu2Norm(q));
      if (!selfHit && kids.length === 0) return null;
      /* A parent that matches keeps its whole child list; otherwise only the
         children that matched are shown. */
      return { node: n, children: selfHit ? (n.children || []) : kids };
    }).filter(Boolean);
    return { section: s, nodes };
  }).filter(x => x.nodes.length > 0);

  const allKeys = hsu2FlatKeys(adminViewer);
  const grantedCount = allKeys.filter(k => grants[k]).length;

  if (row.lvl !== 4) {
    /* Faithful absence, not a bug to route around: getNestedUserPermissions()
       implements ONLY the CUSTOMER_CARE case, so [] is returned for levels 1 and
       6 and the real tab renders zero checkboxes. No tree is invented here. */
    const flat = row.lvl === 1
      ? [["support_users", "Gestione utenti"], ["support_messages", "Messaggi"], ["support_report_affiliate", "Report Affiliation"]]
      : [["support_players", "Players"], ["support_users", "Users"], ["support_sport_coupons", "Sports Coupons"], ["support_sport_bet", "Bet"], ["support_vouchers", "Vouchers"], ["support_withdrawalRequests", "Withdrawal requests"], ["support_messages", "Messages"], ["support_send_messages", "Send messages"], ["support_reportPlayers", "Player Report"], ["support_reportAgents", "Agents Report"], ["support_reportTransactions", "Transactions Report"], ["support_withdrawalRequestsApprovation", "Approval of withdrawal requests"], ["support_withdrawalRequestsPayment", "Payment withdrawal requests"], ["support_reportJackpot", "Jackpots report"]];
    return (
      <div className="hsu2-permempty">
        <Icon name="shield" size={26} />
        <h4>No permissions are assignable to {hsu2Role(row.lvl).name} accounts here</h4>
        <p>
          <code>User::getNestedUserPermissions()</code> builds a tree only for the CUSTOMER_CARE case, so it returns
          an empty array for level {row.lvl} and this tab renders zero checkboxes on the live platform. Nothing has
          been invented to fill the gap.
        </p>
        <p>
          A flat list for this level does exist in <code>UsersController::userPerms()</code>, but only the
          non-support screen <code>/users/{"{id}"}/permissions</code> renders it:
        </p>
        <div className="hsu2-permempty__list">
          {flat.map(([k, l]) => <span key={k}><Hsu2Key>{k}</Hsu2Key> {l}</span>)}
        </div>
        <p className="hsu2-permempty__sug">
          {/* SUGGESTION: either point supportpermissions.blade.php at userPerms(...)["perms"]
              for AFFILIATE/ADMINISTRATION rows, or hide the Permissions tab for them —
              today it is a tab that always renders empty. */}
          Suggested fix recorded in the source: render the flat list here, or hide the tab for these two roles.
        </p>
        {row.lvl === 6 && (
          <p className="hsu2-permempty__sug">
            Note that several of those Administration keys are never checked by <code>checkUserBoPerm</code> at all
            (support_send_messages, support_reportPlayers, support_reportAgents, support_reportTransactions,
            support_withdrawalRequests) — ticking them would change nothing.
          </p>
        )}
      </div>
    );
  }

  return (
    <div className="hsu2-perm">
      <div className="hsu2-permbar">
        <div className="hsu2-permbar__search">
          <Icon name="search" size={13} />
          <input className="input" placeholder="Search permissions, keys or what they gate…" value={q} onChange={e => setQ(e.target.value)} />
          {q && <button className="hsu2-permbar__x" onClick={() => setQ("")} title="Clear"><Icon name="x" size={11} /></button>}
        </div>
        <div className="hsu2-permbar__count"><b>{grantedCount}</b> of {allKeys.length} granted</div>
        <div className="hsu2-permbar__acts">
          <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={() => setCollapsed({})}>Expand all</button>
          <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={() => setCollapsed(Object.fromEntries(sections.map(s => [s.id, true])))}>Collapse all</button>
        </div>
      </div>

      <div className="hsu2-permgrid">
        {visibleSections.map(({ section: s, nodes }) => {
          const keys = s.nodes.reduce((a, n) => a.concat([n.key], n.children || []), []);
          const on = keys.filter(k => grants[k]).length;
          const isCollapsed = !!collapsed[s.id] && !q;
          return (
            <section key={s.id} className={`hsu2-pgroup${isCollapsed ? " is-collapsed" : ""}`}>
              <header className="hsu2-pgroup__head">
                <button className="hsu2-pgroup__toggle" onClick={() => setCollapsed(c => Object.assign({}, c, { [s.id]: !c[s.id] }))}
                  title={isCollapsed ? "Expand" : "Collapse"} disabled={!!q}>
                  <Icon name={isCollapsed ? "chevron_right" : "chevron_down"} size={13} />
                </button>
                <div className="hsu2-pgroup__title">
                  {s.title}
                  {s.inferredTitle && <i className="hsu2-inf">{/* label inferred */}grouping inferred</i>}
                  {s.note && <Tip size={12}>{s.note}</Tip>}
                </div>
                <span className={`hsu2-pgroup__count${on === keys.length ? " is-full" : on === 0 ? " is-zero" : ""}`}>{on}/{keys.length}</span>
                <div className="hsu2-pgroup__acts">
                  <button disabled={readOnly} onClick={() => setMany(keys, true)}>All</button>
                  <button disabled={readOnly} onClick={() => setMany(keys, false)}>None</button>
                </div>
              </header>
              {!isCollapsed && (
                <div className="hsu2-pgroup__body">
                  {nodes.map(({ node, children }) => (
                    <div key={node.key} className="hsu2-pnode">
                      <Hsu2PermRow pkey={node.key} granted={grants[node.key]} onToggle={toggle} readOnly={readOnly} />
                      {children && children.length > 0 && (
                        <div className="hsu2-kids">
                          {children.map(c => (
                            <Hsu2PermRow key={c} pkey={c} granted={grants[c]} onToggle={toggle} readOnly={readOnly} child
                              dimmed={HSU2_PERMS[node.key] && HSU2_PERMS[node.key].master && !grants[node.key]} />
                          ))}
                          {HSU2_PERMS[node.key] && HSU2_PERMS[node.key].master && !grants[node.key] && (
                            <div className="hsu2-kidsnote">
                              <Icon name="alert" size={11} /> <b>{HSU2_PERMS[node.key].label}</b> is off, so every child above is inert —
                              each report endpoint checks the master switch AND its own key.
                            </div>
                          )}
                        </div>
                      )}
                    </div>
                  ))}
                </div>
              )}
            </section>
          );
        })}
        {visibleSections.length === 0 && (
          <div className="hsu2-permnone"><Icon name="search" size={18} /> No permission matches “{q}”.</div>
        )}
      </div>

      <details className="hsu2-unassign">
        <summary>Seven permission strings are checked in code but assignable nowhere</summary>
        <p>
          They have no key in the tree above and none in any <code>userPerms()</code> flat list, so a Customer Care,
          Affiliate or Administration account can only receive them through a hand-written row in the
          <code> permissions</code> table.
        </p>
        <ul>{HSU2_UNASSIGNABLE.map(([k, why]) => <li key={k}><Hsu2Key>{k}</Hsu2Key> — {why}</li>)}</ul>
      </details>
    </div>
  );
};

/* ==================================================================
   DETAIL — GET /supportusers/{id}/ (Home) and /permissions
   ================================================================== */
const Hsu2Detail = ({ row, onBack, onSaved }) => {
  const TABS = [["home", "Home", ""], ["permissions", "Permissions", "permissions"], ["providers", "Providers", "providers"]];
  const [tab, setTab] = window.useUrlTab("/settings/support", TABS, "home");
  const [v, setV] = useStateHsu2(() => ({ firstname: row.firstname, lastname: row.lastname, email: "", country_birth: "", country_residence: "" }));
  const [skins, setSkins] = useStateHsu2(() => row.skins.slice());
  const [tz, setTz] = useStateHsu2(HSU2_TZ[0]);
  const [showPwd, setShowPwd] = useStateHsu2(false);
  const [pwd, setPwd] = useStateHsu2({ a: "", b: "" });
  const set = (k, val) => setV(s => Object.assign({}, s, { [k]: val }));

  /* No grant state here any more. The enforced permissions live in
     `Hsu2Enforced`, which reads them from the database and writes each toggle
     through set_user_permission immediately — so there is nothing to stage,
     nothing to diff and nothing to discard. The upstream tree below it is
     reference and holds no state at all. */

  /* The Providers tab exists in supporttemplate.blade.php:73-80 for AFFILIATE
     rows only, and points at a route that is commented out (routes/admin.php:
     600-602) → 404. Rendered disabled rather than silently dropped or faked. */
  const providersTab = row.lvl === 1;
  const shownTabs = TABS.filter(t => t[0] !== "providers" || providersTab);
  const activeTab = tab === "providers" && !providersTab ? "home" : tab;

  const saveHome = () => {
    if (skins.length === 0) { hsu2Toast("Assign at least one skin", true); return; }
    if (pwd.a && pwd.a !== pwd.b) { hsu2Toast("The two passwords do not match", true); return; }
    if (pwd.a && (pwd.a.length < 6 || pwd.a.length > 30)) { hsu2Toast("Password must be 6–30 characters", true); return; }
    /* KNOWN BUG (evident intent implemented): saveEditUser's home case always
       recomputes blocked/cash_block from checkbox presence (:3981-3982) while
       the support Home form renders no such checkboxes — a plain save therefore
       writes 0 and silently unblocks the account. Nothing here touches them. */
    onSaved(Object.assign({}, row, { firstname: v.firstname, lastname: v.lastname, skins: skins.slice() }));
    hsu2Toast(`Saved — POST /saveUser/${row.id}/home`);
  };

  /* `savePerms` is gone with the staged grant state. It emitted a toast naming
     a POST that was never made and a grant count taken from invented data —
     a save button that reported success for a write that did not exist. Each
     enforced permission now writes itself through set_user_permission the
     moment it is toggled, and useHrsSave refetches before it says "saved". */

  return (
    <div className="page report-page host-players hsu2">
      <div className="hp-edit-head">
        <button className="hp-back" onClick={onBack} title="Back to the operator list"><Icon name="chevron_left" size={18} /></button>
        <div>
          <div className="page__title hsu2-ptitle">Edit {hsu2Role(row.lvl).name}</div>
          <div className="hsu2-idline">
            {row.username}
            <span className="hsu2-idline__id">User ID: {row.id}</span>
            <Hsu2Role lvl={row.lvl} />
          </div>
        </div>
      </div>

      <div className="hp-tabs hsu2-tabs">
        {shownTabs.map(([id, lab]) => (
          <button key={id} className={`hp-tab ${activeTab === id ? "active" : ""}${id === "providers" ? " hsu2-tab--dead" : ""}`}
            onClick={() => { if (id === "providers") { hsu2Toast("Providers tab route is commented out — the real link 404s", true); return; } setTab(id); }}
            title={id === "providers" ? "Route commented out (routes/admin.php:600-602) → 404" : undefined}>
            {lab}{id === "providers" && <span className="hsu2-tab__dead">404</span>}
          </button>
        ))}
      </div>

      <div className="hsu2-tabnote">
        <Icon name="info" size={12} />
        <span>
          <b>showSupportUserDetails</b> runs <code>checkParentPerm()</code> before rendering the Home tab, but
          <b> showSupportUserPermissions does not</b> (:4484-4509) — the Permissions tab has no hierarchy check at all.
          The create-form and save endpoints have no isadmin()/isSkinAdmin() gate beyond the group's auth middleware either.
          {/* SUGGESTION: add the same checkParentPerm() guard to
              showSupportUserPermissions and to the newXxxForm / saveNewXxx endpoints. */}
        </span>
      </div>

      {activeTab === "home" && (
        <>
          <Explainer compact title="What the Home tab saves, in plain English"
            bullets={[
              <>The <b>username is disabled</b> — it is rendered without a name attribute, so it never posts back through saveEditUser.</>,
              <>A <b>password change is optional</b>; leaving both boxes empty keeps the current one. When filled it must be 6–30 characters and match.</>,
              <><b>Personal data</b> is one shared block with the create forms. A Customer Care viewer without <code>support_user_personal_data</code> sees it masked, and the server discards those fields regardless.</>,
              <><b>Skin re-assignment</b> re-derives skin_id and parent_id: one skin re-parents to that skin's admin, several leave skin_id 0 / parent_id 1.</>,
              <>The <code>user_path</code> is rebuilt on save for every role <b>except Customer Care</b>.</>,
            ]}>
            <code>POST /saveUser/{row.id}/home</code> — the shared user editor, reached here through the Support-users list.
          </Explainer>

          <Hsu2Callout tone="ok" icon="shield" title="Block flags are left alone.">
            On the live platform this save recomputes <code>blocked</code> and <code>cash_block</code> from checkbox
            presence, but the support Home form renders no such checkboxes — so a routine save writes 0 and silently
            unblocks the account. This prototype implements the evident intent and does not touch either flag.
            {/* SUGGESTION: only write blocked / cash_block in saveEditUser's home case
                when the request actually carries those inputs. */}
          </Hsu2Callout>

          <div className="hsu2-home">
            <div className="hsu2-home__main">
              <section className="hp-card">
                <div className="hp-card__title">Login data</div>
                <div className="hsu2-form3">
                  <Hsu2Field label="Username" note="Disabled on edit — never posts back">
                    <input className="input" value={row.username} disabled />
                  </Hsu2Field>
                  <Hsu2Field label="New password" note="Leave empty to keep the current password">
                    <div className="hp-pwd">
                      <input className="input" type={showPwd ? "text" : "password"} value={pwd.a} onChange={e => setPwd(p => Object.assign({}, p, { a: e.target.value }))} />
                      <button className="hp-eye" onClick={() => setShowPwd(s => !s)} title={showPwd ? "Hide" : "Show"}><Icon name="eye" size={14} /></button>
                    </div>
                  </Hsu2Field>
                  <Hsu2Field label="Confirm new password">
                    <input className="input" type={showPwd ? "text" : "password"} value={pwd.b} onChange={e => setPwd(p => Object.assign({}, p, { b: e.target.value }))} />
                  </Hsu2Field>
                </div>
              </section>

              <section className="hp-card">
                <div className="hp-card__title">Assignment</div>
                <div className="hsu2-form2">
                  <Hsu2Field label={row.lvl === 6 ? "Skin" : "Skins"} req
                    note={row.lvl === 6 ? "Administration rows carry a single skin_id." : "Stored as multiple_skins. Re-assignment is available to an admin editing an Affiliate or Customer Care row that was not created by a subnet user."}>
                    <Hsu2SkinPicker multi={row.lvl !== 6} value={skins} onChange={setSkins} />
                  </Hsu2Field>
                  <Hsu2Field label="Timezone">
                    <select className="select" value={tz} onChange={e => setTz(e.target.value)}>{HSU2_TZ.map(t => <option key={t}>{t}</option>)}</select>
                  </Hsu2Field>
                </div>
              </section>

              <section className="hp-card">
                <Hsu2Personal v={v} set={set} errs={{}} masked={false} />
              </section>

              <div className="hsu2-savebar hsu2-savebar--static">
                <span>Per-method <code>user_payment_settings</code> are rewritten on save, and the cached user path / skin ids are flushed.</span>
                <button className="rpt-btn rpt-btn--blue hsu2-fbtn" onClick={saveHome}><Icon name="check" size={13} /> Save</button>
              </div>
            </div>

            <aside className="hsu2-home__side">
              <section className="hp-card hp-info">
                <div className="hp-card__title">Info</div>
                {[
                  ["Role", `${hsu2Role(row.lvl).name} (level ${row.lvl})`],
                  ["Skins", row.skins.join(", ") || "—"],
                  ["Parent", row.parent],
                  ["Last access", row.last ? hsu2Date(row.last) : "Never"],
                  ["Registration date", hsu2Date(row.reg)],
                ].map(([k, val]) => <div className="hp-info__row" key={k}><span className="k">{k}</span><span className="v">{val}</span></div>)}
                <div className="hsu2-note">{/* label inferred */}The real Info box hardcodes Italian labels (“Ultimo accesso”, “Data registrazione”, “Mai”) regardless of locale.</div>
              </section>
              <section className="hp-card">
                <div className="hp-card__title">API credentials</div>
                <div className="hsu2-note">A generated <code>api_key</code> and <code>api_token</code> are written at create time by both working save methods. They are not editable from this screen.</div>
              </section>
            </aside>
          </div>
        </>
      )}

      {activeTab === "permissions" && (
        <>
          <Explainer compact title="What this tab actually controls, in plain English"
            bullets={[
              <><b>A ticked box is a row.</b> The <code>permissions</code> table stores one row per (user, permission string); presence is the grant, and the save is a diff — new rows are inserted, cleared ones deleted.</>,
              <><b>The gates only bind three roles.</b> <code>checkUserBoPerm</code> returns <code>true</code> immediately for any account that is not Affiliate(1), Customer Care(4) or Administration(6) — every other role passes regardless of what is ticked here.</>,
              <><b>One inverted flag.</b> <code>support_disable_transfers</code> takes access away instead of granting it.</>,
              <><b>One master switch.</b> <code>support_report</code> is AND-ed with each individual report key, so clearing it disables all twelve children at once.</>,
              <>The support view posts no <code>vpermissions</code> — the per-level transfer value-permissions live on the non-support <code>/users/{"{id}"}/permissions</code> screen.</>,
            ]}>
            <code>POST /saveUser/{row.id}/permissions</code> — the tree comes from <code>User::getNestedUserPermissions()</code>, checked state from <code>checkUserPerm()</code>.
          </Explainer>

          <Hsu2Enforced row={row} />

          <div className="hsu2-refnote">
            <Icon name="info" size={12} />
            <span>
              The tree below is <b>upstream's</b> permission model, kept as reference and
              <b> not checked by this platform</b>. Its keys have no rows behind them, so it
              renders read-only rather than offering ticks that would change nothing. The
              permissions this platform enforces are the ones above.
            </span>
          </div>

          <Hsu2PermTree row={row} grants={HSU2_NO_GRANTS} setGrants={hsu2NoOp}
                        adminViewer={HSU2_ME.isAdmin} readOnly />
        </>
      )}
    </div>
  );
};

/* ==================================================================
   LIST — GET /supportusers/ + GET /getSupportUsers/
   ================================================================== */
const HSU2_EMPTY_FILTERS = { id: "", name: "", lastname: "", username: "", skin: "", last: { from: "", to: "" } };

const SetSupportUsers = () => {
  window.useLocale && window.useLocale();
  /* One request per staff level: PostgREST has no OR across a single column
     without `or=(...)`, and the registry deliberately has no free-form filter
     escape hatch. Three declared reads are clearer than one clever one. */
  const feed = useHrsFetch(() => Promise.all(
    HSU2_STAFF_LEVELS.map(lv => window.sb.list("supportUsers", { limit: 200, filters: { level: lv } }))
  ).then(rs => {
    const bad = rs.find(r => !r.ok);
    if (bad) return bad;
    return { ok: true, data: rs.flatMap(r => r.data), meta: {}, source: "live" };
  }), []);
  const rows = useMemoHsu2(() => (feed.data || []).map(hsu2Row), [feed.data]);
  const [draft, setDraft] = useStateHsu2(HSU2_EMPTY_FILTERS);
  const [applied, setApplied] = useStateHsu2(HSU2_EMPTY_FILTERS);
  const [sort, setSort] = useStateHsu2({ key: "id", dir: "desc" });
  const [page, setPage] = useStateHsu2(0);
  const [pageSize, setPageSize] = useStateHsu2(50);
  const [cols, setCols] = useStateHsu2({ lastname: false, firstname: false });  // real columnDefs: hidden by default
  const [colPop, setColPop] = useStateHsu2(false);
  const [create, setCreate] = useStateHsu2(null);
  const [confirmDel, setConfirmDel] = useStateHsu2(null);
  const [open, setOpen] = useStateHsu2(null);

  const skinFilterVisible = HSU2_ME.isAdmin;                     // supportindex :101-112
  const skinColumnVisible = HSU2_ME.lvl < 2;                     // supportindex :133-135

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "Exact match", tip: "Matched with users.id = value — not a LIKE." },
    { key: "name", label: "Name", type: "text", icon: "user", placeholder: "Contains…", tip: "users.firstname LIKE %value%" },
    { key: "lastname", label: "Lastname", type: "text", icon: "user", placeholder: "Contains…", tip: "users.lastname LIKE %value%" },
    { key: "username", label: "Username", type: "text", icon: "user", placeholder: "Starts with…", tip: "users.username LIKE value% — a prefix match, so searching a middle fragment finds nothing." },
    { key: "skin", label: "Skin", type: "select", icon: "flag", placeholder: "Select", options: HSU2_SKINS, hidden: !skinFilterVisible, tip: "Rendered only for isadmin() viewers." },
    {
      key: "last", label: "Last access", type: "daterange", icon: "calendar", grow: true,
      tip: "On the live platform only a complete range works: with a single bound the query references un-joined aliases and errors out. Either bound alone filters correctly here.",
    },
  ];

  const filtered = useMemoHsu2(() => {
    const f = applied;
    const from = f.last && f.last.from ? f.last.from : "";
    const to = f.last && f.last.to ? f.last.to : "";
    return rows.filter(r => {
      if (f.id && String(r.id) !== String(f.id).trim()) return false;
      if (f.name && !hsu2Norm(r.firstname).includes(hsu2Norm(f.name))) return false;
      if (f.lastname && !hsu2Norm(r.lastname).includes(hsu2Norm(f.lastname))) return false;
      // prefix match — mirrors `users.username LIKE value%`
      if (f.username && hsu2Norm(r.username).indexOf(hsu2Norm(f.username)) !== 0) return false;
      if (f.skin && !r.skins.includes(f.skin)) return false;
      /* KNOWN BUG fixed per policy: the real single-bound branches reference
         un-joined aliases and throw. Each bound is honoured independently here. */
      if (from || to) {
        if (!r.last) return false;
        const d = hsu2IsoDay(r.last);
        if (from && d < from) return false;
        if (to && d > to) return false;
      }
      return true;
    });
  }, [rows, applied]);

  const sorted = useMemoHsu2(() => {
    const s = filtered.slice();
    const dir = sort.dir === "asc" ? 1 : -1;
    s.sort((a, b) => {
      if (sort.key === "username") return a.username.localeCompare(b.username) * dir;
      if (sort.key === "last") return ((a.last || 0) - (b.last || 0)) * dir;
      return (a.id - b.id) * dir;   // every other column falls back to users.id
    });
    return s;
  }, [filtered, sort]);

  const paged = sorted.slice(page * pageSize, page * pageSize + pageSize);
  useEffectHsu2(() => { setPage(0); }, [applied, pageSize]);

  const skinCell = (r) => r.skins.length > 1
    ? <span className="hsu2-skincell">{r.skins[0]}<i title={r.skins.join(", ")}>+{r.skins.length - 1}</i></span>
    : (r.skins[0] || "-");

  const COLUMNS = [
    { key: "id", label: "ID", sortable: true, width: 92, firstDir: "desc" },
    {
      key: "username", label: "Username", sortable: true, align: "left", firstDir: "asc",
      render: r => (
        <span className="hsu2-unamecell">
          <button className="rpt-user" onClick={() => setOpen(r)} title={`Open /supportusers/${r.id}/`}>
            <Icon name="user" size={11} /> {r.username} <Icon name="chevron_right" size={12} className="chev" />
          </button>
          <button className="hsu2-imp" title="Impersonate — rendered when the impersonate policy passes and the viewer is not already impersonating"
            onClick={(e) => { e.stopPropagation(); hsu2Toast(`Impersonate ${r.username}`); }}>
            <Icon name="external" size={11} />
          </button>
        </span>
      ),
    },
    { key: "lastname", label: "Lastname", align: "left", hidden: !cols.lastname },
    { key: "role", label: "Role", render: r => <Hsu2Role lvl={r.lvl} /> },
    { key: "firstname", label: "Name", align: "left", hidden: !cols.firstname },
    { key: "skin", label: "Skin", align: "left", hidden: !skinColumnVisible, render: skinCell },
    { key: "parent", label: "Parent", align: "left" },
    { key: "last", label: "Last access", sortable: true, align: "left", firstDir: "desc", render: r => r.last ? hsu2Date(r.last) : "-" },
    {
      key: "actions", label: "Actions", width: 108,
      render: r => (
        <div className="hp-list-actions">
          {HSU2_ME.isAdmin && (
            <button className="hp-act hp-act--danger" title="Delete — GET /users/delete/{id}" onClick={(e) => { e.stopPropagation(); setConfirmDel(r); }}><Icon name="trash" size={13} /></button>
          )}
          <button className="hp-act hp-act--edit" title="View / edit" onClick={(e) => { e.stopPropagation(); setOpen(r); }}><Icon name="edit" size={13} /></button>
        </div>
      ),
    },
  ];

  const exportCols = COLUMNS.filter(c => c.key !== "actions" && !c.hidden).map(c => ({
    key: c.key, label: c.label,
    get: (r) => c.key === "role" ? hsu2Role(r.lvl).name
      : c.key === "skin" ? r.skins.join(" / ")
        : c.key === "last" ? (r.last ? hsu2Date(r.last) : "-")
          : r[c.key],
  }));

  if (open) {
    const live = rows.find(r => r.id === open.id) || open;
    /* onSaved used to patch a local array. Editing a staff account is a write
       and there is no write path yet (stage 7), so it reports instead. */
    return <Hsu2Detail row={live} onBack={() => setOpen(null)}
             onSaved={(next) => hsu2Toast("Not saved — no write path yet",
               `Would update users id ${next.id}. Reads are live; writes land in stage 7.`)} />;
  }

  return (
    <HrsShell
      title="Support users"
      subtitle="Operators — the back-office's own Customer Care, Administration and Affiliate accounts"
      gate={<>
        No <code>checkUserBoPerm</code> gate at all — access is role/skin-flag based:
        <code> isadmin()</code>, <code>isSkinAdmin()</code>, or the <code>enable_agents_operators</code> skin
        setting. Every support endpoint re-checks it and 403s via the deliberate
        <code> authorize('asdasdas')</code> idiom.
      </>}
      gateNote={<>{" "}The sidebar wraps the flag in <code>!isCustomCare()</code>; the server check does not.</>}
      explainer={{
        title: "What this screen is, in plain English",
        bullets: [
          <>Three account types only — <b>Customer Care(4)</b>, <b>Administration(6)</b> and <b>Affiliate(1)</b>. Everything else in the network lives on the Users screen.</>,
          <>Each row opens a two-tab editor: <b>Home</b> (the shared personal-data form) and <b>Permissions</b> (the <code>support_*</code> checkbox tree).</>,
          <>The tree is only implemented for Customer Care. Affiliate and Administration rows open a Permissions tab that is genuinely empty — reproduced, not papered over.</>,
          <><b>The gates bind three roles and no others.</b> <code>checkUserBoPerm</code> returns <code>true</code> immediately for any account outside levels 1 / 4 / 6, so nothing ticked here constrains a Master, Agent, Shop, Skin admin or Super admin.</>,
          <>A Customer Care account of a flagged skin is hidden from the sidebar but <b>not</b> blocked from the URL — the sidebar's <code>!isCustomCare()</code> wrapper has no server-side counterpart.</>,
          <>No KPIs, no bulk actions and no working global search: the real screen has none of the three. The DataTables search box renders but the server never reads it, so it is not reproduced here.</>,
          <>Demo session is <b>Super admin</b>, which is why the Skin column, all three create buttons, the Delete action and the isadmin()-only Settings branch of the permission tree are visible.</>,
        ],
      }}
      actions={<>
        <button className="rpt-btn rpt-btn--blue hsu2-newbtn" onClick={() => setCreate("cc")}><Icon name="plus" size={12} /> New Customer Care</button>
        {HSU2_ME.lvl < 2 && <button className="rpt-btn rpt-btn--blue hsu2-newbtn" onClick={() => setCreate("adm")}><Icon name="plus" size={12} /> New Administration<span className="hsu2-badbadge" title="POST /saveNewAmministrazione has no controller method — 500 on the live platform">500</span></button>}
        {HSU2_ME.isAdmin && <button className="rpt-btn rpt-btn--blue hsu2-newbtn" onClick={() => setCreate("aff")}><Icon name="plus" size={12} /> New Affiliate</button>}
      </>}>

      <HrsFilters
        fields={FIELDS}
        values={draft}
        onChange={(k, val) => setDraft(d => Object.assign({}, d, { [k]: val }))}
        onSearch={(vals) => setApplied(vals)}
        onReset={() => { setDraft(HSU2_EMPTY_FILTERS); setApplied(HSU2_EMPTY_FILTERS); }}
        resultLabel={`${sorted.length} of ${rows.length}`}>
        <div className="hrs-fcard hsu2-colcard">
          <div className="hrs-flab"><Icon name="list" size={11} /> Columns</div>
          <button className="hrs-fctl hsu2-colbtn" onClick={() => setColPop(o => !o)}>
            {(cols.lastname ? 1 : 0) + (cols.firstname ? 1 : 0) === 0 ? "Default (2 hidden)" : "Custom"} <Icon name="chevron_down" size={11} />
          </button>
          {colPop && (
            <>
              <div className="hsu2-colscrim" onClick={() => setColPop(false)} />
              <div className="hsu2-colpop">
                <div className="hsu2-colpop__head">Hidden by the real columnDefs</div>
                <label><input type="checkbox" checked={cols.lastname} onChange={() => setCols(c => Object.assign({}, c, { lastname: !c.lastname }))} /> Lastname</label>
                <label><input type="checkbox" checked={cols.firstname} onChange={() => setCols(c => Object.assign({}, c, { firstname: !c.firstname }))} /> Name</label>
                <div className="hsu2-colpop__foot">Skin renders only for viewers below Skin-admin level, and is not toggleable.</div>
              </div>
            </>
          )}
        </div>
      </HrsFilters>

      <div className="hsu2-sortnote">
        <Icon name="info" size={12} /> Only <b>ID</b>, <b>Username</b> and <b>Last access</b> are sortable — the
        server-side switch handles those three and silently falls back to <code>users.id ASC</code> for every other
        column, so the rest are not offered as sortable headers here.
      </div>

      <HrsAsync state={feed} skeletonRows={8} skeletonCols={6}
                empty="No staff accounts yet. Support users are users at level 1, 4 or 6.">
        {() => (<>
      <HrsTable
        columns={COLUMNS}
        rows={paged}
        sort={sort}
        onSort={setSort}
        rowKey="id"
        empty="No operator matches these filters."
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b onClick={() => setOpen(r)}>{r.username}</b>
              <Hsu2Role lvl={r.lvl} />
            </div>
            <div className="hrs-card__grid">
              <span>ID</span><b>{r.id}</b>
              <span>Last access</span><b>{r.last ? hsu2Date(r.last) : "-"}</b>
              {skinColumnVisible && <><span>Skin</span><b>{r.skins.join(", ") || "-"}</b></>}
            </div>
            <details className="hsu2-cardmore">
              <summary>More</summary>
              <div className="hrs-card__grid">
                <span>Name</span><b>{r.firstname || "-"}</b>
                <span>Lastname</span><b>{r.lastname || "-"}</b>
                <span>Parent</span><b>{r.parent || "—"}</b>
              </div>
            </details>
            <div className="hsu2-cardacts">
              <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={() => setOpen(r)}><Icon name="edit" size={12} /> Open</button>
              {HSU2_ME.isAdmin && <button className="rpt-btn rpt-btn--reset hsu2-fbtn hsu2-fbtn--danger" onClick={() => setConfirmDel(r)}><Icon name="trash" size={12} /> Delete</button>}
            </div>
          </>
        )}
      />
        </>)}
      </HrsAsync>

      <HrsPager page={page} pageSize={pageSize} total={sorted.length} onPage={setPage} onPageSize={setPageSize} sizes={[5, 10, 25, 50]} />

      <HrsExport
        count={sorted.length}
        filename="support_users.csv"
        note="The live button is a client-side DataTables PDF export of the visible columns on the current page only; this one covers the whole filtered set."
        onCsv={() => hsu2Csv(sorted, exportCols, "support_users.csv")} />

      {create && (
        <Hsu2CreateModal
          kind={create}
          existing={rows}
          onClose={() => setCreate(null)}
          onCreate={(row) => {
            setCreate(null);
            hsu2Toast("Not created — no write path yet",
              `Would insert into users: username "${row.username}", user_level ${row.lvl}, under the caller's subtree. Reads are live; writes land in stage 7.`);
          }} />
      )}

      {confirmDel && (
        <Hsu2Modal
          title={`Delete ${confirmDel.username}?`}
          sub={`GET /users/delete/${confirmDel.id}/ · rendered only when the client user.is_admin flag is 1`}
          onClose={() => setConfirmDel(null)}
          footer={<>
            <button className="rpt-btn rpt-btn--reset hsu2-fbtn" onClick={() => setConfirmDel(null)}>Cancel</button>
            <button className="rpt-btn rpt-btn--blue hsu2-fbtn hsu2-fbtn--danger"
              onClick={() => { hsu2Toast("Not deleted — no write path yet",
                `Would set users.deleted_at on id ${confirmDel.id} (${confirmDel.username}). Soft delete — the row stays and the Deleted Users screen reads it.`); setConfirmDel(null); }}>
              <Icon name="trash" size={13} /> Delete
            </button>
          </>}>
          <p className="hsu2-modaltext">
            The account is soft-deleted (<code>users.deleted = 1</code>) and disappears from this list, which filters on
            <code> deleted = 0</code>. It then shows up on the Deleted Users screen.
          </p>
          <Hsu2Callout tone="warn" icon="alert" title="One-way.">
            There is no restore anywhere in the platform — the Deleted Users screen offers no such action, and none is
            invented here.
          </Hsu2Callout>
        </Hsu2Modal>
      )}
    </HrsShell>
  );
};

window.SetSupportUsers = SetSupportUsers;
