// Represents: admin.users.deleted · UsersController — see docs/ISYSTEM_REFERENCE.md §Batch 1 "Deleted Users"
/* Deleted Users (Settings ▾) — the standalone screen behind GET /deleted_users
   (routes/admin.php L500-502 → UsersController::deletedIndex L78-83, which sets
   $request->deleted = true and delegates to index(); the same Blade view as
   /users, branching on $deleted). Data feed: GET /getUsers/{parent_id}/?deleted=1
   → getUsersList L746, where $table = 'deleted_users' and the query runs as
   DeletedUser::on('mysql_ro') under SET SESSION TRANSACTION ISOLATION LEVEL
   READ UNCOMMITTED. Column config: App\Services\UserListService::tableColumns(
   $deleted, isadmin()||isCustomCare(), !isCustomCare()).

   Read-only by construction — nothing is added here that the platform lacks:
   - no FormRequest (DataTables params are consumed ad hoc in getUsersList);
   - no create buttons (the "New <role>" header buttons are skipped entirely in
     deleted mode, index.blade.php L39-41);
   - no row actions (the server sets disable_actions = true, L1170, and the JS
     renderer returns '' — the Actions column stays, permanently empty);
   - no bulk actions (select:true is set on the DataTable, nothing consumes it);
   - no detail/drill-down (no route exists for a deleted row);
   - NO RESTORE. No route, controller method or UI element anywhere in the
     platform moves a row from deleted_users back to users (verified by grep
     across routes/admin.php, UsersController and the users views). Deletion is
     one-way from the backoffice, so no Restore is offered here either.

   Two things this rebuild does differently from the live page, both fixing a
   gap rather than adding a feature:
   - FILTERS APPLY ON SEARCH. The live inputs feed DataTables per-column search
     slots and only reach the server when #kt_search is clicked; this page kept
     a live-on-change filter with a Reset button and no Search. Now `draft`
     (what you type) and `applied` (what Search sent) are separate, Search
     applies, Reset clears both — the real request semantics.
   - COLUMN VISIBILITY. email / firstname / lastname are real columns of the
     feed sent with visible:false and filterable but never renderable, so a
     search on Email cannot be read on the live screen. They are built here,
     hidden by default exactly as the feed has them, and a Columns popover (the
     affordance HostPlayers.jsx / HostSportCoupons.jsx already carry for their
     own tables) turns them on. See HDU2_COLS.
   The PDF export button stays visible but DISABLED: the real one is a
   client-side DataTables/pdfmake button with no server endpoint behind it, and
   nothing here renders a PDF. The CSV export is a real download and is live.

   Alignment: src/pages/HostUsers.jsx renders this same deleted view as a mode
   switch on the /users list (both are the real screen's two entry points).
   Columns, labels, role chips, block-state chips and the known-bug divergences
   are kept identical there and here; this file differs only in page chrome — it
   uses the Hrs* shell (src/report-shell.jsx) for the header/gate tip, filter
   strip, table and pager, so the Settings ▾ leaf matches the rest of the
   rebuilt screens. Its own CSS is prefixed `hdu-`. */

const { useState: hduUseState, useMemo: hduUseMemo } = React;

/* ---------------- helpers ---------------- */
const hduRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
const hduPad = (n) => String(n).padStart(2, "0");
const hduParse = (s) => (s ? Date.parse(s.replace(" ", "T") + "Z") : null);
/* deleted_at renders d/m/Y H:i and last_login d/m/Y G:i (getUsersList L1024). */
const hduDate = (ts) => {
  if (ts == null) return "-";
  const d = new Date(ts);
  return `${hduPad(d.getUTCDate())}/${hduPad(d.getUTCMonth() + 1)}/${d.getUTCFullYear()} ${hduPad(d.getUTCHours())}:${hduPad(d.getUTCMinutes())}`;
};
const hduMoney = (cur, n) => `${cur} ${Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const hduNorm = (s) => String(s || "").trim().toLowerCase();

/* usersLevels() (UsersController L1188-1218) — values are users.user_level,
   copied verbatim into deleted_users.user_level. Customer Care(4) /
   Administration(6) / Affiliate(1) are excluded from this screen's fixed query
   constraints and from the User Type filter; Player(30) appears here because
   the `user_level < PLAYER_LEVEL` restriction applies only in non-deleted mode
   (L964-966), so this list mixes deleted players and deleted network users.
   Shop/Promoter/Agent/Master names are overridable per skin via the
   custom_{shop,promoter,agent,master}_name settings (suppressed for
   super-admin viewers — the only viewers who can reach this screen). */
const HDU_ROLES = [
  { lvl: 0, name: "Super Admin", tone: "#181c32" },
  { lvl: 2, name: "Skin Access", tone: "#28387a" },   // key usertype_admin_new, slug "Admin"
  { lvl: 8, name: "Master", tone: "#4b1fb3" },
  { lvl: 9, name: "Regulation User", tone: "#3a4047" },
  { lvl: 10, name: "Agent", tone: "#0aa19a" },
  { lvl: 15, name: "Promoter", tone: "#e6a82c" },
  { lvl: 20, name: "Shop", tone: "#1f9d57" },
  { lvl: 30, name: "Player", tone: "#7a8194" },
];
const hduRoleName = (lvl) => { const r = HDU_ROLES.find(x => x.lvl === lvl); return r ? r.name : `Level ${lvl}`; };

/* Skin filter options come from $user->getSkins() (rendered only for
   isadmin() || isCustomCare(), view L100-111). Same operator skin list the
   /users screen uses. */
const HDU_SKINS = [
  { name: "Jokerenvivo", cur: "ARS" }, { name: "Donjoker", cur: "ARS" }, { name: "apuestadepana", cur: "CLP" },
  { name: "Juegojoker", cur: "ARS" }, { name: "win24hs", cur: "ARS" }, { name: "apostando365", cur: "PYG" },
  { name: "Tucasino", cur: "ARS" }, { name: "Jugaygana", cur: "ARS" }, { name: "PlaySpin", cur: "BOB" },
  { name: "Anchodeespada", cur: "ARS" }, { name: "tuapuesta", cur: "ARS" },
];
/* $user->getCommissionProfiles() — operator data, not committed to the repo. */
const HDU_PROFILES = ["Commissions 30%", "Commissions 25%", "Commissions 20%", "Cost only 10%", "Cost + fee 12%"];
/* deleted_by ids resolved to usernames via a live User lookup (L1025). */
const HDU_DELETERS = ["admin", "jugayganaadmin", "win24hsadmin", "admin", "tucasinoAdmin"];

/* Deterministic dataset for `deleted_users`. Rows are produced by the
   User::boot() `deleting` hook (app/Models/User.php L440-453): the full users
   row is copied into deleted_users with deleted_by = Auth::user()->id and
   old_user_id = $user->id, balance_history is purged and the users row is then
   HARD-deleted. `id` is therefore the NEW deleted_users PK, not the original
   user id. The first 14 rows use the same seed and generator as HostUsers.jsx's
   deleted dataset so both entry points into this table show the same records;
   the tail extends the history so the 50-per-page server-side pager is real. */
/* ---------- the row source ------------------------------------------------
   Was 132 invented rows from a seeded RNG — names, balances, deleters, skins,
   all fabricated. Now `users` filtered to deleted_at is not null.

   That filter is declared as `fixed` on the resource, not passed by this
   screen, because it is part of what the resource IS. isystem's own Deleted
   Users export forgets the equivalent condition and returns LIVE users; making
   it the screen's responsibility is how that happens.

   Two columns the real screen shows have no counterpart here, and are rendered
   as "—" rather than invented:
     · deletedBy — isystem's deleted_users table carries deleted_by. Our soft
       delete keeps the row in `users` and records only deleted_at, so there is
       nobody to name. Recording it is a schema change, not a screen change.
       <!-- SUGGESTION: add users.deleted_by, so a deletion has an author. -->
     · profile  — commission profile lives in user_commission_profile_assignments
       and is not joined here yet.

   `id` is the user's own id. isystem shows the deleted_users PK, which collides
   with a different live user's id when clicked — the divergence already noted
   in this file's header. */
const hduRow = (u) => ({
  id: u.id,
  oldId: u.id,
  lvl: u.user_level,
  username: u.username,
  skin: (u.skin && u.skin.name) || String(u.skin_id),
  cur: u.currency || (u.skin && u.skin.currency) || "",
  bal: 0,          // balances are not carried for a deleted user
  credits: 0,
  parent: (u.parent && u.parent.username) || null,
  deletedBy: null, // no such column — see above
  deletedAt: u.deleted_at ? Date.parse(u.deleted_at) : null,
  last: u.last_login_at ? Date.parse(u.last_login_at) : null,
  cashBlock: !!u.cash_blocked,
  userBlock: !!u.blocked,
  profile: "",
  firstname: u.firstname,
  lastname: u.lastname,
  email: u.email,
});

/* ---------------- small atoms (share the /users chip vocabulary) ---------------- */
const HduRoleChip = ({ lvl }) => {
  const r = HDU_ROLES.find(x => x.lvl === lvl);
  return <span className="hu-rolechip" style={{ background: r ? r.tone : "#7a8194" }}>{hduRoleName(lvl)}<i>{lvl}</i></span>;
};
const HduGateChip = ({ children }) => <code className="hu-gate">{children}</code>;

/* Cash block / User block. The live screen still renders working toggles here,
   wired to showAggiornaDBcashBlock(row.id) / the block modals using the
   deleted_users PK — which collides with a DIFFERENT live user's id if clicked
   (a marked leftover). Evident intent implemented: a read-only state chip.
   <!-- SUGGESTION: render the block columns read-only on /deleted_users (or bind them to old_user_id and guard against id reuse) instead of shipping live toggles wired to the deleted_users primary key. --> */
const HduBlockChip = ({ on, label }) => <span className={`chip ${on ? "chip--err" : "chip--neutral"}`}>{on ? label : "—"}</span>;

/* Balance cell — currency + formattaCurrency(balance) plus the hardcoded-Italian
   "Fido:" credits line (getUsersList L1031-1046). The refresh / transfer buttons
   the live list shows are omitted in deleted mode by the controller itself. */
const HduBalance = ({ row }) => (
  <div className="hdu-bal">
    <b className={row.bal < 0 ? "hdu-neg" : ""}>{hduMoney(row.cur, row.bal)}</b>
    <span className="hdu-fido">Fido: {Number(row.credits || 0).toFixed(2)}</span>
  </div>
);

/* ---------------- column visibility ----------------
   The real feed sends email / firstname / lastname as real columns with
   `visible:false` (ajax.js L192-199) and offers NO way to reveal them — an
   operator filtering by email cannot see what matched. The three columns are
   built here and default to hidden exactly as the feed does; the Columns
   popover (the same affordance HostPlayers.jsx and HostSportCoupons.jsx
   already carry) lets them be shown. Visibility is client-side session state,
   like the DataTables colvis state on the screens that do have it.
   <!-- SUGGESTION: give the deleted/live users DataTable the column-visibility
        control the players and sport-coupons tables already have (players uses
        GET /playerTableSettingForm, coupons GET /sportTableSettingForm) — today
        email / firstname / lastname / profilo_provvigionale are filterable but
        unreachable, so a filtered result cannot be read. -->
   `profilo_provvigionale` is the fourth such column; it is filter-only in the
   real payload (no cell is ever built for it, UserListService L27-58), so it
   is filterable here and stays out of the column list rather than being
   invented as a cell. */
const HDU2_COLS = [
  { key: "id", label: "ID", always: true },
  { key: "oldId", label: "Old user id" },
  { key: "username", label: "Username", always: true },
  { key: "role", label: "Role" },
  { key: "email", label: "Email", feed: true },
  { key: "firstname", label: "Name", feed: true },
  { key: "lastname", label: "Lastname", feed: true },
  { key: "deletedBy", label: "Deleted by" },
  { key: "deletedAt", label: "Deleted at" },
  { key: "skin", label: "Skin" },
  { key: "parent", label: "Parent" },
  { key: "balance", label: "Balance" },
  { key: "subnet", label: "Subnet balance" },
  { key: "last", label: "Last access" },
  { key: "cash", label: "Cash block" },
  { key: "user", label: "User block" },
  { key: "actions", label: "Actions", always: true },
];
/* Default = what the real table renders: everything except the feed's three
   visible:false columns. */
const HDU2_COLS_DEFAULT = HDU2_COLS.filter(c => !c.feed).map(c => c.key);

const Hdu2ColsMenu = ({ visible, onToggle, onReset, onClose }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const onEsc = (e) => { if (e.key === "Escape") onClose(); };
    const t = setTimeout(() => { document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onEsc); }, 0);
    return () => { clearTimeout(t); document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onEsc); };
  }, [onClose]);
  return (
    <div ref={ref} role="dialog" aria-label="Columns"
      style={{ position: "absolute", zIndex: 90, top: "calc(100% + 6px)", right: 0, width: 258, background: "#fff", border: "1px solid var(--border-default)", borderRadius: 10, boxShadow: "0 24px 48px -12px rgba(15,20,32,.22)", overflow: "hidden", textAlign: "left" }}>
      <div style={{ padding: "10px 12px", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 8, background: "var(--n-25)" }}>
        <Icon name="settings" size={12} style={{ color: "var(--text-secondary)" }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 12.5, fontWeight: 700 }}>Columns</div>
          <div style={{ fontSize: 10.5, color: "var(--text-tertiary)" }}>Shown for this session only</div>
        </div>
        <button onClick={onClose} title="Close"
          style={{ width: 22, height: 22, padding: 0, border: "none", borderRadius: 5, background: "transparent", color: "var(--text-tertiary)", cursor: "pointer", display: "grid", placeItems: "center" }}>
          <Icon name="x" size={10} />
        </button>
      </div>
      <div style={{ padding: "6px 4px", maxHeight: 330, overflow: "auto" }}>
        {HDU2_COLS.map(c => {
          const on = c.always || visible.indexOf(c.key) >= 0;
          return (
            <label key={c.key}
              style={{ display: "flex", alignItems: "center", gap: 10, padding: "6px 10px", borderRadius: 6, cursor: c.always ? "default" : "pointer" }}>
              <input type="checkbox" disabled={c.always} checked={on} onChange={() => onToggle(c.key)} style={{ accentColor: "var(--p-500)", margin: 0 }} />
              <span style={{ flex: 1, fontSize: 12.5, fontWeight: on ? 600 : 500, color: on ? "var(--text-primary)" : "var(--text-secondary)" }}>{c.label}</span>
              {c.always && <em style={{ fontSize: 9.5, fontStyle: "normal", fontWeight: 700, letterSpacing: ".04em", color: "var(--text-tertiary)" }}>REQUIRED</em>}
              {c.feed && <em style={{ fontSize: 9.5, fontStyle: "normal", fontWeight: 700, letterSpacing: ".04em", color: "var(--p-600)" }} title="Sent by the feed with visible:false — hidden by default on the real screen">FEED</em>}
            </label>
          );
        })}
      </div>
      <div style={{ padding: "8px 10px", borderTop: "1px solid var(--border-subtle)", background: "var(--n-25)", display: "flex", alignItems: "center", gap: 8 }}>
        <button onClick={onReset}
          style={{ padding: "5px 10px", border: "1px solid var(--border-default)", borderRadius: 6, background: "#fff", fontSize: 11.5, fontWeight: 600, color: "var(--text-secondary)", cursor: "pointer" }}>
          Reset to defaults
        </button>
        <span style={{ fontSize: 10.5, color: "var(--text-tertiary)" }}>FEED = <code>visible:false</code></span>
      </div>
    </div>
  );
};

/* The three permission layers this screen passes through, surfaced verbatim. */
const HduGateRail = () => (
  <div className="hdu-gaterail">
    <span className="hdu-gaterail__item">
      <Icon name="list" size={12} /> <b>Nav</b>
      <HduGateChip>isadmin()</HduGateChip>
      <Tip>The Settings ▾ dropdown opens for <code>isadmin() || isSkinAdmin() || $enable_agents_operators</code> (sidebar.blade.php L504, where enable_agents_operators = isSkinAdmin() || (!isCustomCare() &amp;&amp; checkSkinSett(skin_id, "enable_agents_operators"))) — but this entry sits inside a nested <code>@if (isadmin())</code> (L522), so only super-admins ever see it. No skin feature flag gates the item itself.</Tip>
    </span>
    <span className="hdu-gaterail__item">
      <Icon name="shield" size={12} /> <b>Controller</b>
      <HduGateChip>support_users</HduGateChip>
      <Tip>UsersController::index() L87-88 403s when <code>isAffiliate() || !checkUserBoPerm(id,"support_users") || isRegulator()</code>. checkUserBoPerm (L2589) only really reads the permissions table for Affiliate(1) / Customer Care(4) / Administration(6) — every other level auto-passes.</Tip>
    </span>
    <span className="hdu-gaterail__item">
      <Icon name="lock" size={12} /> <b>Data feed</b>
      <HduGateChip>deleted=1 → isadmin()</HduGateChip>
      <Tip>GET /getUsers/{"{parent_id}"}/?deleted=1 hard-requires isadmin() (getUsersList L765-766); anyone else gets the literal string "not allowed" back instead of a DataTables payload.</Tip>
    </span>
    <span className="hdu-gaterail__item hdu-gaterail__item--muted">
      <Icon name="flag" size={12} /> <b>Skin flags</b> none
      <Tip>No skin setting gates this screen. <code>enable_agents_operators</code> only widens the enclosing Settings ▾ dropdown for non-admins, who still cannot see this item.</Tip>
    </span>
  </div>
);

/* ---------------- filters ---------------- */
/* All eleven inputs from the view (L72-187) drive per-column DataTables search
   → WHEREs in getUsersList; every one defaults to empty. The server also
   supports a data_creazione / addedTime range (L823-843) that this page renders
   no input for — noted, not built. */
const HDU_EMPTY_FILTERS = {
  id: "", name: "", lastname: "", email: "", username: "",
  skin: "", role: "", profile: "", parent: "", deletedBy: "", last: { from: "", to: "" },
};

const SetDeletedUsers = () => {
  const feed = useHrsFetch(() => window.sb.list("deletedUsers", { limit: 200 }), []);
  const rows = hduUseMemo(() => (feed.data || []).map(hduRow), [feed.data]);
  /* APPLY-ON-SEARCH. The real screen does not filter as you type: each input
     writes into a DataTables per-column search slot and nothing is sent until
     the `#kt_search` button is clicked (users/ajax.js), which triggers one
     server-side redraw. `draft` is what the operator is typing, `applied` is
     what the last Search sent — the table only ever reads `applied`. Reset
     clears both (the shared kit's Reset Filters button). */
  const [draft, setDraft] = hduUseState(HDU_EMPTY_FILTERS);
  const [applied, setApplied] = hduUseState(HDU_EMPTY_FILTERS);
  /* Client default order is [[0,"desc"]] → ID desc (ajax.js L91). */
  const [sort, setSort] = hduUseState({ key: "id", dir: "desc" });
  const [page, setPage] = hduUseState(0);
  const [pageSize, setPageSize] = hduUseState(50); // pageLength 50, lengthMenu [5,10,25,50]
  const [cols, setCols] = hduUseState(HDU2_COLS_DEFAULT);
  const [colsOpen, setColsOpen] = hduUseState(false);

  const setField = (k, v) => setDraft(s => ({ ...s, [k]: v }));
  const search = (v) => { setApplied(v); setDraft(v); setPage(0); };
  const reset = () => { setDraft(HDU_EMPTY_FILTERS); setApplied(HDU_EMPTY_FILTERS); setPage(0); };
  const toggleCol = (k) => setCols(c => c.indexOf(k) >= 0 ? c.filter(x => x !== k) : [...c, k]);
  const colShown = (k) => cols.indexOf(k) >= 0;

  const parentOpts = hduUseMemo(() => Array.from(new Set(rows.map(r => r.parent))).sort(), [rows]);

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "search", placeholder: "Exact ID", tip: "Exact match — the server applies `deleted_users.id = ?`, not LIKE. This is the new deleted_users primary key, not the original user id (filter that one by hand: it is only a display column)." },
    { key: "username", label: "Username", type: "text", icon: "user", placeholder: "Contains…", tip: "LIKE %…% on deleted_users.username." },
    { key: "name", label: "Name", type: "text", icon: "user", placeholder: "Contains…", tip: "LIKE %…% on deleted_users.firstname. Name / Lastname / Email are columns of the data feed but ship hidden by default (ajax.js L192-199) — searchable but never rendered on the live screen. Turn them on under Columns to read what matched." },
    { key: "lastname", label: "Lastname", type: "text", icon: "user", placeholder: "Contains…", tip: "LIKE %…% on deleted_users.lastname (hidden column, searchable)." },
    { key: "email", label: "Email", type: "text", icon: "mail", placeholder: "Contains…", tip: "LIKE %…% on deleted_users.email (hidden column, searchable)." },
    { key: "skin", label: "Skin", type: "select", icon: "grid", placeholder: "Select", options: HDU_SKINS.map(s => s.name), tip: "Rendered only for isadmin() || isCustomCare() viewers; options come from $user->getSkins(). Joined from the skins table (L979-982)." },
    { key: "role", label: "User Type", type: "select", icon: "users", placeholder: "- Select -", options: HDU_ROLES.map(r => ({ value: String(r.lvl), label: `${r.name} (${r.lvl})` })), tip: "usersLevels() minus Customer Care(4) / Administration(6) / Affiliate(1). Player(30) is offered here because the user_level < 30 restriction applies only in the live view — this table holds deleted players and deleted network users alike." },
    { key: "profile", label: "Commissions profile", type: "select", icon: "percent", placeholder: "- Select -", options: HDU_PROFILES, tip: "Filters deleted_users.profilo_provvigionale — a column the DataTable renders with visible:false purely so it can be filtered." },
    { key: "parent", label: "Parent", type: "select", icon: "users", placeholder: "- Select -", options: parentOpts, tip: "select2 AJAX feed → route admin.users.search (/users2), user_types [0,2,8,10,15,20], scoped hierarchy-safe through Auth::user()->getChilds(true). Server matches deleted_users.parent_id; the list resolves it to the parent username via a LEFT JOIN on users AS u_parent." },
    { key: "deletedBy", label: "Deleted by", type: "select", icon: "trash", placeholder: "- Select -", options: Array.from(new Set(HDU_DELETERS)), tip: "Rendered only when isadmin() && $deleted (view L164-172) — same select2 source as Parent. Server matches deleted_users.deleted_by = ?; the column resolves the id to a username with a live User lookup." },
    { key: "last", label: "Last access", type: "daterange", icon: "calendar", tip: "Single calendar feeding two hidden inputs joined from|to (ajax.js L242-249) → last_login BETWEEN / >= / <= (L868-889). The server also accepts a data_creazione (addedTime) range, but this page renders no input for it." },
  ];

  /* Deleted mode skips checkParentPerm and all user_path scoping (L753-770),
     so there is no "direct children of the current parent" rule here: a
     super-admin sees the deleted users of every skin and every network. Fixed
     constraints still applied server-side: deleted = 0 (L944), levels 4/6/1
     excluded and id > 1 (L939-942). */
  const filtered = hduUseMemo(() => rows.filter(r => {
    const f = applied;
    if (f.id && String(r.id) !== f.id.trim()) return false;
    if (f.username && hduNorm(r.username).indexOf(hduNorm(f.username)) < 0) return false;
    if (f.name && hduNorm(r.firstname).indexOf(hduNorm(f.name)) < 0) return false;
    if (f.lastname && hduNorm(r.lastname).indexOf(hduNorm(f.lastname)) < 0) return false;
    if (f.email && hduNorm(r.email).indexOf(hduNorm(f.email)) < 0) return false;
    if (f.skin && r.skin !== f.skin) return false;
    if (f.role && r.lvl !== Number(f.role)) return false;
    if (f.profile && r.profile !== f.profile) return false;
    if (f.parent && r.parent !== f.parent) return false;
    if (f.deletedBy && r.deletedBy !== f.deletedBy) return false;
    if (f.last && f.last.from && (!r.last || r.last < hduParse(f.last.from + " 00:00"))) return false;
    if (f.last && f.last.to && (!r.last || r.last > hduParse(f.last.to + " 23:59"))) return false;
    return true;
  }), [rows, applied]);

  /* ORDER BY is honored server-side for id / credits / bonus / username /
     last_login only (L893-936) — credits and bonus target columns this table no
     longer shows, and any other clicked column silently keeps
     deleted_users.id ASC. Only the three that really sort are marked sortable. */
  const sorted = hduUseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    const s = filtered.slice();
    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)) || (a.id - b.id)) * dir;
      return (a.id - b.id) * dir;
    });
    return s;
  }, [filtered, sort]);

  const pages = Math.max(1, Math.ceil(sorted.length / pageSize));
  const pageSafe = Math.min(page, pages - 1);
  const pageRows = sorted.slice(pageSafe * pageSize, pageSafe * pageSize + pageSize);

  /* Export — the live Excel button posts to /users/excel (admin.users.export) →
     UsersController::exportList → User::exportList, which queries the LIVE users
     table only; excelExport() in ajax.js (L416-439) never sends the `deleted`
     flag, so on this screen the download silently contains live users instead of
     the deleted rows on screen. Known-bug policy: the evident intent is
     implemented here — the export contains exactly the filtered deleted rows,
     with the three deleted-only columns added and Subnet Balance dropped
     (it is meaningless for a deleted row, see the column comment below).
     <!-- SUGGESTION: pass the `deleted` flag through excelExport() / exportList() so the Deleted Users screen exports deleted_users rows instead of live users. --> */
  const exportCsv = () => hrsCsv(sorted, [
    { key: "id", label: "ID" },
    { key: "oldId", label: "Old user id" },
    { key: "username", label: "Username" },
    { key: "role", label: "Role", get: r => hduRoleName(r.lvl) },
    { key: "deletedBy", label: "Deleted by" },
    { key: "deletedAt", label: "Deleted at", get: r => hduDate(r.deletedAt) },
    { key: "skin", label: "Skin" },
    { key: "parent", label: "Parent" },
    { key: "balance", label: "Balance", get: r => hduMoney(r.cur, r.bal) },
    { key: "last", label: "Last access", get: r => (r.last ? hduDate(r.last) : "-") },
    { key: "cash", label: "Cash block", get: r => (r.cashBlock ? "Yes" : "No") },
    { key: "user", label: "User block", get: r => (r.userBlock ? "Yes" : "No") },
  ], "deleted_users_export.csv");

  /* PDF is a client-side DataTables/pdfmake button relocated into the filter bar
     (#users_dt_slot). It produces no file here: nothing in this prototype
     renders a PDF, and no server route does either — the real one is pure
     browser-side pdfmake, and it covers only the CURRENT DataTables page.
     Rendered disabled with that stated, rather than firing a toast that claims
     an export happened. The CSV button beside it downloads for real.
     <!-- SUGGESTION: feed the pdfmake button the full filtered result set (or server-render the PDF — there is no PDF endpoint today) instead of the current DataTables page. --> */

  /* `hidden` is honoured by HrsTable (it filters the column list), so the
     Columns popover drives the table by flipping it. ID / Username / Actions
     are pinned (`always` in HDU2_COLS) and never carry it. */
  const columns = [
    {
      key: "id", label: "ID", sortable: true, firstDir: "desc",
      render: r => <span className="hdu-id">{r.id}</span>,
    },
    {
      /* deleted_users.old_user_id — the original users.id (UserListService
         L29-30). Deleted-mode-only column. */
      key: "oldId", hidden: !colShown("oldId"),
      label: <React.Fragment>Old user id <Tip size={12}>deleted_users.old_user_id — the id the row had in the users table before deletion. The ID column to its left is the new deleted_users primary key. {/* label inferred */}The key backend.old_user_id resolves in no committed lang file, so this label is inferred.</Tip></React.Fragment>,
      render: r => <span className="hdu-id hdu-muted">{r.oldId}</span>,
    },
    {
      /* Plain text in deleted mode — no subnet drill-down link (L1010-1015).
         The impersonate icon is prepended only when can('impersonate', $row)
         passes, and it cannot: the row is a DeletedUser, UserPolicy is only
         conventionally discovered for App\Models\User and no DeletedUserPolicy
         exists, so the ability resolves to deny and the icon never renders. */
      key: "username", label: "Username", sortable: true, firstDir: "asc",
      render: r => <span className="hdu-user">{r.username}</span>,
    },
    { key: "role", label: "Role", hidden: !colShown("role"), render: r => <HduRoleChip lvl={r.lvl} /> },
    {
      /* deleted_users.email — a real column of the feed, sent with
         visible:false (ajax.js L192-199) and unreachable on the live screen.
         Off by default here too; the Columns popover reveals it. */
      key: "email", hidden: !colShown("email"),
      label: <React.Fragment>Email <Tip size={12}>deleted_users.email. The feed sends this column with <code>visible:false</code>, so the live table never shows it even though the Email filter above searches it (<code>email LIKE %…%</code>). Shown here because you turned it on.</Tip></React.Fragment>,
      render: r => <span className="hdu-muted">{r.email}</span>,
    },
    {
      /* deleted_users.firstname — label "Name", matching the filter above and
         UserListService's key. Same visible:false story as Email. */
      key: "firstname", hidden: !colShown("firstname"),
      label: <React.Fragment>Name <Tip size={12}>deleted_users.firstname, the column the <b>Name</b> filter searches (<code>firstname LIKE %…%</code>). Sent by the feed with <code>visible:false</code> and hidden by default, exactly like the real screen.</Tip></React.Fragment>,
      render: r => r.firstname,
    },
    {
      key: "lastname", hidden: !colShown("lastname"),
      label: <React.Fragment>Lastname <Tip size={12}>deleted_users.lastname — searched by the Lastname filter (<code>lastname LIKE %…%</code>), sent with <code>visible:false</code>, hidden by default.</Tip></React.Fragment>,
      render: r => r.lastname,
    },
    {
      key: "deletedBy", hidden: !colShown("deletedBy"),
      label: <React.Fragment>Deleted by <Tip size={12}>deleted_users.deleted_by resolved to a username through a live User lookup (L1025) — the operator who triggered GET /users/delete/{"{id}"}/. Deleted-mode-only column. {/* label inferred */}backend.deleted_by resolves in no committed lang file; label inferred.</Tip></React.Fragment>,
      render: r => r.deletedBy,
    },
    {
      key: "deletedAt", hidden: !colShown("deletedAt"),
      label: <React.Fragment>Deleted at <Tip size={12}>deleted_users.deleted_at, formatted d/m/Y H:i. The migration declares it via softDeletes()-&gt;useCurrent(), but the model does NOT use the SoftDeletes trait — it is simply a CURRENT_TIMESTAMP column recording when the copy was made. Not sortable: the server honors ORDER BY on id / username / last_login only and silently falls back to deleted_users.id ASC for everything else. {/* label inferred */}</Tip></React.Fragment>,
      render: r => hduDate(r.deletedAt),
    },
    { key: "skin", label: "Skin", hidden: !colShown("skin"), render: r => r.skin },
    { key: "parent", label: "Parent", hidden: !colShown("parent"), render: r => r.parent },
    { key: "balance", label: "Balance", hidden: !colShown("balance"), render: r => <HduBalance row={r} /> },
    {
      /* The live cell calls getSubnetBalance($row->id) against the LIVE users
         tree using the deleted_users PK, so it sums a random unrelated subnet
         (a marked bug). Evident intent: no subnet figure for a deleted row.
         <!-- SUGGESTION: skip the getSubnetBalance() call in deleted mode instead of resolving the deleted_users primary key against the live users tree. --> */
      key: "subnet", hidden: !colShown("subnet"),
      label: <React.Fragment>Subnet balance <Tip size={12}>Blank by design here. On the live screen this cell computes getSubnetBalance() from the live users tree using the deleted_users primary key — a meaningless number for a deleted row, so the prototype shows none.</Tip></React.Fragment>,
      render: () => <span className="hdu-dash">—</span>,
    },
    {
      key: "last", label: "Last access", sortable: true, firstDir: "desc", hidden: !colShown("last"),
      render: r => (r.last ? hduDate(r.last) : "-"),
    },
    { key: "cash", label: "Cash block", hidden: !colShown("cash"), render: r => <HduBlockChip on={r.cashBlock} label="Blocked" /> },
    { key: "user", label: "User block", hidden: !colShown("user"), render: r => <HduBlockChip on={r.userBlock} label="Blocked" /> },
    {
      /* Always empty: getUsersList sets disable_actions = true for deleted rows
         (L1170) and the ajax.js renderer returns '' (L123-124). The column is
         kept because the real table keeps it — no edit, no delete, and above
         all no restore, which exists nowhere in the platform. */
      key: "actions",
      label: <React.Fragment>Actions <Tip size={12}>Permanently empty. The server sets disable_actions = true for every deleted row and the client renderer returns an empty string — Edit and Delete only exist in the live view. There is no restore, purge or detail route for a deleted row anywhere in the platform, so nothing is offered here.</Tip></React.Fragment>,
      align: "center",
      render: () => <span className="hdu-dash">—</span>,
    },
  ];

  const explainer = (
    <Explainer compact title="Where these rows come from — and why nothing here is reversible" bullets={[
      <React.Fragment key="a">Deletion is a copy, not a soft delete: <code>User::boot()</code>'s <code>deleting</code> hook writes the whole users row into <b>deleted_users</b> (adding <code>old_user_id</code> and <code>deleted_by</code>), purges its balance history, and the users row is then <b>hard-deleted</b>. The trigger is <code>GET /users/delete/{"{id}"}/</code> on the /users screen, gated <code>isadmin() || isSkinAdmin()</code>.</React.Fragment>,
      <React.Fragment key="b"><b>No restore exists</b> — no route, controller method or button anywhere returns a row to the users table. Nor is there a purge or a detail page: this screen is a read-only ledger of removals.</React.Fragment>,
      <React.Fragment key="c">Scope: deleted mode skips <code>checkParentPerm</code> and all user_path scoping, so a super-admin sees every skin and every network at once. Rows are still filtered to <code>deleted = 0</code> — a user whose <code>users.deleted</code> flag was already 1 at deletion time never appears — and Customer Care(4) / Administration(6) / Affiliate(1) levels plus <code>id &lt;= 1</code> are always excluded.</React.Fragment>,
      <React.Fragment key="d">Leftovers from the shared view are shown honestly: the block columns are read-only state (the live toggles are wired to the deleted_users id and would hit an unrelated live user), Subnet balance is blank, and the Actions column stays empty. Reads run on <code>mysql_ro</code> under READ UNCOMMITTED.</React.Fragment>,
    ]}>
      Every row is a user that was removed from the platform. The table is the separate <b>deleted_users</b> table — a near-full clone of <code>users</code> — not a filtered view of live accounts, which is why balances, blocks and parents are frozen at the moment of deletion.
    </Explainer>
  );

  return (
    <HrsShell
      title="Deleted users"
      subtitle="GET /deleted_users · admin.users.deleted → UsersController::deletedIndex() re-runs index() with $deleted = true · data feed GET /getUsers/{parent_id}/?deleted=1"
      gate={["isadmin()", "support_users"]}
      gateNote={<React.Fragment> The sidebar entry sits inside a nested <code>@if (isadmin())</code>, and the DataTables feed hard-requires <code>isadmin()</code> whenever <code>deleted=1</code> — it answers the literal string "not allowed" to anyone else. {/* label inferred */}The nav label itself resolves only to the raw key <code>backend.deleted_users</code> in the committed lang files (storage/lang is gitignored), so "Deleted users" is an inferred operator-facing label — as are Old user id, Deleted by and Deleted at.</React.Fragment>}
      explainer={explainer}
      actions={
        <span style={{ position: "relative", display: "inline-flex" }}>
          <button className="hrs-btn hrs-btn--search" onClick={() => setColsOpen(o => !o)}
            title="Show / hide columns — including the three the feed sends hidden">
            <Icon name="settings" size={13} /> Columns
            <span style={{ marginLeft: 6, fontSize: 11, fontWeight: 600, opacity: .85 }}>
              {HDU2_COLS.filter(c => c.always || colShown(c.key)).length}/{HDU2_COLS.length}
            </span>
          </button>
          {colsOpen && (
            <Hdu2ColsMenu
              visible={cols}
              onToggle={toggleCol}
              onReset={() => setCols(HDU2_COLS_DEFAULT)}
              onClose={() => setColsOpen(false)}
            />
          )}
        </span>
      }
    >
      <HduGateRail />

      <HrsFilters
        fields={FIELDS}
        values={draft}
        onChange={setField}
        onSearch={search}
        onReset={reset}
        resultLabel={`${sorted.length.toLocaleString()} of ${rows.length.toLocaleString()}`}
      />

      <HrsExport
        count={sorted.length}
        twoPhase
        filename="deleted_users_export.csv"
        onCsv={exportCsv}
        note={
          <React.Fragment>
            {/* Kept visible because the real screen has it (it documents the
                surface), but disabled: it produces no file here. */}
            <button
              className="hdu-pdfbtn"
              disabled
              aria-disabled="true"
              style={{ opacity: .5, cursor: "not-allowed" }}
              title="Not wired: needs the client-side DataTables/pdfmake bundle — there is no PDF endpoint on the platform. Use Export CSV."
            >
              <Icon name="receipt" size={12} /> PDF
            </button>
            <span className="hdu-exportnote">
              Excel → <code>POST /users/excel</code> (admin.users.export), button gated by <code>$can_export</code> — true for the super-admins who can open this screen.
              <Tip size={12}>Divergence on purpose: the live export never sends the `deleted` flag, so it returns LIVE users on this screen. The prototype exports the deleted rows actually shown — the Export CSV button really downloads them. <b>PDF is disabled:</b> the real one is a browser-side DataTables/pdfmake button (no server route exists) covering only the current page; nothing here renders a PDF, so it does nothing rather than pretending to.</Tip>
            </span>
          </React.Fragment>
        }
      />

      <div className="hdu-source">
        <span className="hdu-source__main">
          <Icon name="trash" size={13} /> <code>deleted_users</code>
          <span className="hdu-source__sub">all skins &amp; networks · user_path scoping skipped · mysql_ro, READ UNCOMMITTED</span>
        </span>
        <span className="hdu-nores"><Icon name="alert" size={11} /> one-way — no restore exists</span>
        <span className="hdu-hint">
          Email · Name · Lastname ship hidden by default — turn them on under <b>Columns</b>.
          <Tip size={12}>ajax.js sets visible:false on those three columns; they are still part of the feed and of the per-column search, so the filters work even though no cell is rendered. The real screen offers no way to reveal them — the Columns control here does (the same affordance the Players and Sport coupons tables already have), which is why a search on Email can now be read.</Tip>
        </span>
        {/* <!-- SUGGESTION: give the three hidden columns (email / firstname / lastname) a column-visibility control, so an operator filtering by email can see what matched. Implemented in this prototype as the Columns popover in the page header. --> */}
      </div>

      <HrsAsync state={feed} skeletonRows={10} skeletonCols={7}
                empty="No deleted users. Nothing has been soft-deleted yet.">
        {() => (<>
      <HrsTable
        columns={columns}
        rows={pageRows}
        sort={sort}
        onSort={(next) => { setSort(next); setPage(0); }}
        rowKey="id"
        empty="No deleted users match these filters."
        renderCard={r => (
          <React.Fragment>
            {/* Own class, not .hrs-card__top: this page must not restyle the
                shared report cards, whose only common ancestor is .hrs-page. */}
            <div className="hdu-cardtop">
              <b className="hdu-user">{r.username}</b>
              <span className="hdu-when">{hduDate(r.deletedAt)}</span>
            </div>
            <div className="hdu-cardchips">
              <HduRoleChip lvl={r.lvl} />
              <span className="chip chip--neutral">by {r.deletedBy}</span>
              {r.cashBlock && <span className="chip chip--warn">Cash block</span>}
              {r.userBlock && <span className="chip chip--err">User block</span>}
            </div>
            <details className="hdu-details">
              <summary>Row detail</summary>
              <div className="hrs-card__grid">
                <span>ID</span><b>{r.id}</b>
                <span>Old user id</span><b>{r.oldId}</b>
                {/* The three feed-hidden columns follow the Columns popover on
                    the card too, so mobile and desktop show the same set. */}
                {colShown("email") && <React.Fragment><span>Email</span><b>{r.email}</b></React.Fragment>}
                {colShown("firstname") && <React.Fragment><span>Name</span><b>{r.firstname}</b></React.Fragment>}
                {colShown("lastname") && <React.Fragment><span>Lastname</span><b>{r.lastname}</b></React.Fragment>}
                <span>Skin</span><b>{r.skin}</b>
                <span>Parent</span><b>{r.parent}</b>
                <span>Balance</span><b>{hduMoney(r.cur, r.bal)}</b>
                <span>Fido</span><b>{Number(r.credits || 0).toFixed(2)}</b>
                <span>Subnet balance</span><b>—</b>
                <span>Last access</span><b>{r.last ? hduDate(r.last) : "-"}</b>
                <span>Actions</span><b>—</b>
              </div>
            </details>
          </React.Fragment>
        )}
      />
        </>)}
      </HrsAsync>

      <HrsPager
        page={pageSafe}
        pageSize={pageSize}
        total={sorted.length}
        onPage={setPage}
        onPageSize={(n) => { setPageSize(n); setPage(0); }}
        sizes={[5, 10, 25, 50]}
      />
    </HrsShell>
  );
};

window.SetDeletedUsers = SetDeletedUsers;
