// 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 /deposits · DepositsController — see docs/ISYSTEM_REFERENCE.md §Batch 1 "Deposits"
/* Host → Deposits — the admin deposit-request queue (`ordini` table).
   Replaces the legacy HostDeposits bundled in src/pages/HostFinanceMsg.jsx (this
   file loads after it, so this definition wins — HostFinanceMsg's copy retires later).

   Real surface: route admin.deposits `GET /deposits` (+ /deposits/getDeposits,
   POST /deposits/approve/{id}, POST /deposits/decline/{id});
   sidebar badge via GET /getPendingCounts polled every 3 s.
   Permission: sidebar gated by checkUserBoPerm('support_deposits') inside the role gate
   isadmin() || isSkinAdmin() || isAdministration() || isCustomCare() || isShop();
   the routes themselves enforce only ['auth','admin','2fa','g2fa'] + hierarchy scoping
   (users.user_path LIKE '<caller path>/%').

   Labels: runtime translations live in storage/lang (gitignored); backend.* keys with
   no committed text (backend.payment_details, backend.last_transaction,
   backend.decline_reason, …) are rendered as sensible operator-facing labels
   ("label inferred" per build policy). The portlet title is hardcoded "Deposits"
   in the real Blade. */

const { useState: useStateHdp, useMemo: useMemoHdp, useEffect: useEffectHdp } = React;

/* ---------------- constants (real values from the reference) ---------------- */

/* ordini.payment_status — DepositsController::getPaymentStatusLabel. No constants
   class in the real code (magic numbers throughout); NOT WithdrawRequestStatus —
   deposits have no ERROR/CANCELED values. Anything else renders "Unknown". */
/* Labels come from deposit_request_statuses; only the chip COLOUR stays here,
   because a colour is a presentation choice and the table has no opinion. */
const HDP_STATUS_CLASS = {
  0: "fm-chip--pending",    // badge-warning
  1: "fm-chip--approved",   // badge-success
  2: "fm-chip--rejected",   // badge-danger
};
/* A row carries its own label from the embedded status row; the list form is
   only needed where there is no row (the filter pill, the export header). */
const hdpStatusOf = (r) => [r.statusLabel || `Status ${r.status}`, HDP_STATUS_CLASS[r.status] || "hdp-chip--unknown"];
const hdpStatusLabel = (statuses, s) => {
  const hit = (statuses || []).find(x => x[0] === Number(s));
  return hit ? hit[1] : `Status ${s}`;
};

/* Method filter options come from payment_methods, which is what
   DepositMethodsController::getDepositMethods reads for the admin's skin. The
   five hardcoded codes that used to live here were the platform's real methods
   at one point in time — which is exactly the kind of list that goes stale
   silently. */

/* Search-type select (user_search_type2). The real server `switch` default case is
   SINGLE account, so the empty "Select" option behaves as an exact-user match;
   it only applies when a user is selected. */
const HDP_SEARCH_TYPES = [
  ["",            "Select"],                 // backend.select_option — behaves as single
  ["all",         "Account + sub-accounts"], // backend.all_selections
  ["subaccounts", "Sub-accounts"],           // backend.subaccounts
  ["single",      "Single account"],         // backend.account
];

const HDP_PAGE_SIZES = [5, 10, 25, 50, 100]; // real DataTables lengthMenu; default 100

/* ---------------- live rows ----------------
   This block used to build 120 deposits from a seeded RNG: twelve player names
   against three shops, ten Spanish-language sender names, five real-sounding
   Spanish rejection reasons ("ya se te cargo, revise antes de solicitar
   deposito"), 22-digit CVU numbers and CUITs. Every one of those is a claim
   about money somebody moved. They are `deposit_requests` rows now, and an
   empty queue means an empty queue. */
const hdpMethodLabel = (code) => { const s = code.replace(/-/g, " "); return s.charAt(0).toUpperCase() + s.slice(1); }; // ucfirst(str_replace('-',' ',…)) like the real row builder
const hdpAmount = (n, cur) => Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " " + cur; // number_format(amount,2).' '.currency
const hdpDate = (ts) => { const d = new Date(ts); const p = (x) => String(x).padStart(2, "0"); return `${p(d.getDate())}/${p(d.getMonth() + 1)}/${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`; }; // d/m/Y H:i

/* deposit_requests -> the row shape this screen renders.
   `lastTx` used to be an N+1 lookup faked over the generated set. isystem runs
   that query per row; here it is derived from the rows already fetched, and
   marked as scoped to them rather than presented as the player's true last
   approved deposit. */
const hdpRow = (r) => ({
  id: Number(r.id),
  user: r.user ? r.user.username : "",
  userId: r.user_id == null ? null : Number(r.user_id),
  parent: r.user && r.user.parent ? r.user.parent.username : "",
  skin: r.skin ? r.skin.name : "",
  skinId: r.skin_id == null ? null : Number(r.skin_id),
  ts: r.created_at ? Date.parse(r.created_at) : null,
  amount: Number(r.amount) || 0,
  currency: r.currency || "",
  method: r.method ? r.method.code : "",
  methodName: r.method ? r.method.name : "",
  status: Number(r.status_id),
  statusLabel: r.status ? r.status.label : null,
  updateTs: r.decided_at ? Date.parse(r.decided_at) : null,
  doneBy: r.decidedBy ? r.decidedBy.username : null,
  reason: r.decline_reason || "",
  /* isystem spread the payer's identity across ~35 PSP-specific columns. Here
     it is payer_name / payer_document plus the provider payload, so a method
     that does not collect a sender simply has none — rather than one invented
     for it. */
  sender: r.payer_name || null,
  recipient: null,
  cvi: r.payer_document || null,
  cuit: null,
  isTest: !!r.is_test,
  ledgerEntryId: r.ledger_entry_id == null ? null : Number(r.ledger_entry_id),
  lastTx: null,
});

const hdpToast = (m) => window.PAYBO?.emitToast && window.PAYBO.emitToast({ id: `hdp-${Date.now()}`, tx_id: m, amount: 0, currency: "HOST", player: "Deposits", reason: "Prototype state only \u2014 not persisted." });

/* ---------------- small pieces ---------------- */

const HdpPill = ({ label, onClear }) => (
  <span className="hdp-pill">
    {label}
    <button onClick={onClear} title="Remove filter"><Icon name="x" size={9} /></button>
  </span>
);

const HdpCopyField = ({ label, value }) => (
  <div className="fm-pd-row">
    <span className="fm-pd-label">{label}</span>
    <span className="fm-pd-val" title={value}>{value}</span>
    <button className="fm-copy" title="Copy to clipboard" onClick={() => {
      if (navigator.clipboard?.writeText) navigator.clipboard.writeText(String(value)).catch(() => {});
      hdpToast("Copied");
    }}><Icon name="copy" size={13} /></button>
  </div>
);

/* Payment details cell — only rendered for wire-argentina (real behavior); the
   cuit "Código Único" block is commented out in the real view and stays hidden. */
const HdpPayDetails = ({ r }) => r.method !== "wire-argentina" ? <span className="hdp-dim">—</span> : (
  <div className="fm-pd hdp-pd">
    <HdpCopyField label="Nombre de quien transfiere" value={r.sender} />
    <HdpCopyField label="Destinatario" value={r.recipient} />
    <HdpCopyField label="CVU/CBU" value={r.cvi} />
  </div>
);

const HdpStatusCell = ({ r }) => {
  const [label, cls] = hdpStatusOf(r);
  return (
    <div className="fm-status">
      <span className={`fm-chip ${cls}`}>{label}</span>
      {r.status !== 0 && (
        <div className="fm-status-sub">{hdpDate(r.updateTs)}<br /><b>{r.doneBy}</b></div>
      )}
    </div>
  );
};

/* Row actions — rendered only when payment_status == 0 AND method !== 'online'
   (real condition). 'online' (PSP-processed) deposits never get buttons but DO
   count toward the pending badge. */
const HdpRowActions = ({ r, onApprove, onDecline, block }) => {
  if (r.status !== 0) return <span className="hdp-dim">—</span>;
  if (r.method === "online") return (
    <span className="hdp-dim" title="Online (PSP-processed) deposits are settled by the payment gateway — no manual approve/reject here. They still count toward the sidebar pending badge.">—</span>
  );
  return (
    <div className="hdp-acts">
      <button className="hdp-act hdp-act--ok" title="Approve request" disabled={block} onClick={() => onApprove(r)}><Icon name="check" size={14} /></button>
      <button className="hdp-act hdp-act--no" title="Reject request" disabled={block} onClick={() => onDecline(r)}><Icon name="x" size={14} /></button>
    </div>
  );
};

/* Approve / Reject confirmation modal (SweetAlert in the real page).
   Full-screen on mobile via .hdp-modal media query. */
const HdpActionModal = ({ mode, row, busy, onConfirm, onClose }) => {
  const [reason, setReason] = useStateHdp("");
  const approve = mode === "approve";
  return (
    <div className="bp-modal-scrim hdp-scrim" onClick={() => { if (!busy) onClose(); }}>
      <div className="hdp-modal" onClick={e => e.stopPropagation()}>
        <div className="hdp-modal-head">
          <span className={`hdp-modal-ic ${approve ? "ok" : "no"}`}><Icon name={approve ? "check" : "x"} size={16} /></span>
          <div>
            <div className="hdp-modal-title">{approve ? "Approve deposit" : "Reject deposit"} #{row.id}</div>
            <div className="hdp-modal-sub">{approve
              ? "Credits the player via an internal transfer (processTransfer, system 'bo'), then runs reward processing."
              : "No money moves — the request is marked Rejected with your reason."}</div>
          </div>
        </div>
        <div className="hdp-modal-body">
          <div className="hdp-summary">
            <div><span className="k">Player</span><span className="v">{row.user}</span></div>
            <div><span className="k">Parent</span><span className="v">{row.parent || "-"}</span></div>
            <div><span className="k">Amount</span><span className="v">{hdpAmount(row.amount, row.currency)}</span></div>
            <div><span className="k">Method</span><span className="v">{hdpMethodLabel(row.method)}</span></div>
            <div><span className="k">Date</span><span className="v">{hdpDate(row.ts)}</span></div>
          </div>
          {!approve && (
            <div className="hdp-reason">
              <label>Decline reason <span className="req">* required</span></label>
              {/* Required client-side only in the real page (SweetAlert inputValidator);
                  the server reads `reason` with fallback 'No reason provided' and does no
                  validation / max length. The prototype keeps the requirement in the UI. */}
              {/* <!-- SUGGESTION: validate `reason` server-side (required + max length) instead of relying on the SweetAlert inputValidator. --> */}
              <textarea autoFocus rows={3} value={reason} onChange={e => setReason(e.target.value)} placeholder="Tell the player why this deposit is being rejected…" />
            </div>
          )}
          {approve && (
            <div className="hdp-modal-note">
              On success the row records who processed it and when; promo-code redemption,
              deposit bonuses and legacy promotions run afterwards — their failures are
              logged but never fail the approval.
            </div>
          )}
        </div>
        <div className="hdp-modal-foot">
          <button className="hdp-btn hdp-btn--ghost" disabled={busy} onClick={onClose}>Cancel</button>
          <button className={`hdp-btn ${approve ? "hdp-btn--ok" : "hdp-btn--no"}`} disabled={busy || (!approve && !reason.trim())}
            onClick={() => onConfirm(reason.trim())}>
            {busy ? "Processing transfer…" : approve ? "Approve request" : "Reject request"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* Filter card wrapper — top-level so its element type stays stable across
   renders (an inline component would remount its inputs on every keystroke). */
const HdpFCard = ({ sheet, grow, children }) => sheet
  ? <div className="hdp-sheet-field">{children}</div>
  : <div className={`hdp-fcard ${grow ? "hdp-fcard--grow" : ""}`}>{children}</div>;

/* One filter control set, rendered inside the desktop hero strip AND the mobile
   full-height sheet (§11: filter bars collapse into a Filters button + sheet).
   `idSuffix` keeps datalist ids unique across the two instances. */
const HdpFilterControls = ({ f, set, sheet, idSuffix, methods, statuses, users }) => {
  const listId = `hdp-user-list-${idSuffix}`;
  return (
    <>
      <HdpFCard sheet={sheet}>
        <div className="filter-hero__label"><Icon name="credit_card" size={11} /> Method
          <Tip>Options come from the skin's enabled deposit methods (deposit_methods ⋈ skin_deposit_methods); matches exactly on the order's payment method code.</Tip>
        </div>
        <select className="filter-hero__select" value={f.method} onChange={e => set({ method: e.target.value })}>
          <option value="">ALL</option>
          {methods.map(([code, name]) => <option key={code} value={code}>{name}</option>)}
        </select>
      </HdpFCard>
      <HdpFCard sheet={sheet}>
        <div className="filter-hero__label"><Icon name="flag" size={11} /> Payment status</div>
        <select className="filter-hero__select" value={f.status} onChange={e => set({ status: e.target.value })}>
          <option value="">ALL</option>
          {statuses.map(([v, label]) => <option key={v} value={String(v)}>{label}</option>)}
        </select>
      </HdpFCard>
      <HdpFCard sheet={sheet}>
        <div className="filter-hero__label"><Icon name="arrow_up" size={11} /> Amount</div>
        <div className="hdp-range">
          <input className="filter-hero__input" placeholder="From" value={f.amtFrom} onChange={e => set({ amtFrom: e.target.value })} />
          <span>—</span>
          <input className="filter-hero__input" placeholder="To" value={f.amtTo} onChange={e => set({ amtTo: e.target.value })} />
        </div>
      </HdpFCard>
      <HdpFCard sheet={sheet}>
        <div className="filter-hero__label"><Icon name="calendar" size={11} /> Date</div>
        <div className="hdp-range">
          <input className="filter-hero__input" type="date" value={f.dateFrom} onChange={e => set({ dateFrom: e.target.value })} />
          <input className="filter-hero__input" type="date" value={f.dateTo} onChange={e => set({ dateTo: e.target.value })} />
        </div>
      </HdpFCard>
      <HdpFCard sheet={sheet} grow>
        <div className="filter-hero__label"><Icon name="user" size={11} /> User
          <Tip>The real page uses a remote user search (select2) over any account in your network, plus a search-type select: <b>Account&nbsp;+&nbsp;sub-accounts</b>, <b>Sub-accounts</b> only, or <b>Single account</b> (also the default when left on "Select"). It only applies once a user is chosen.</Tip>
        </div>
        <div className="hdp-userrow">
          <input className="filter-hero__input" list={listId} placeholder="Search user…" value={f.user} onChange={e => set({ user: e.target.value })} />
          <datalist id={listId}>{users.map(u => <option key={u} value={u} />)}</datalist>
          <select className="filter-hero__select hdp-stype" value={f.searchType} onChange={e => set({ searchType: e.target.value })}>
            {HDP_SEARCH_TYPES.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
          </select>
        </div>
      </HdpFCard>
      <HdpFCard sheet={sheet}>
        <div className="filter-hero__label"><Icon name="search" size={11} /> Payment details
          <Tip>Substring match across the stored wire details: CUIT ("Código Único", stored but not displayed), CVU/CBU and recipient name. The sender name is <b>not</b> searched — same as the real server.</Tip>
        </div>
        {/* Mirrors the real LIKE %…% OR-group across ordini.cuit / cvi / recipient_fullname
            (sender_name intentionally excluded, as on the live platform). */}
        <input className="filter-hero__input" placeholder="CUIT · CVU/CBU · recipient…" value={f.payDetails} onChange={e => set({ payDetails: e.target.value })} />
      </HdpFCard>
    </>
  );
};

const HDP_DEFAULT_FILTERS = { method: "", status: "", amtFrom: "", amtTo: "", dateFrom: "", dateTo: "", user: "", searchType: "", payDetails: "" };

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

  /* processed-action overrides persist so approvals/rejections survive reloads */
  /* CONTENT, not a preference: these are approve/reject decisions. One of the
     five keys pbStore.clearContent() drops at backend cutover (src/store.jsx). */
  const feed = useHrsFetch(() => window.sb.list("depositRequests", { limit: 1000 }), []);
  const save = useHrsSave(feed);
  const methodFeed = useHrsFetch(() => window.sb.list("paymentMethods", { limit: 200 }), []);
  const statusFeed = useHrsFetch(() => window.sb.list("depositStatuses", { limit: 50 }), []);
  const rows = useMemoHdp(() => {
    const mapped = (feed.data || []).map(hdpRow);
    /* "Last transaction" is isystem's per-row N+1: the latest approved deposit
       for the same player. Derived from the rows on this page, and the cell
       says so — a player whose last approved deposit predates this window
       would otherwise appear never to have deposited. */
    const last = {};
    mapped.forEach(r => { if (r.status === 1 && (!last[r.userId] || r.ts > last[r.userId].ts)) last[r.userId] = r; });
    mapped.forEach(r => { const l = last[r.userId]; if (l) r.lastTx = { amount: l.amount, currency: l.currency, ts: l.ts }; });
    return mapped;
  }, [feed.data]);
  const hdpMethods = useMemoHdp(
    () => (methodFeed.data || []).map(m => [m.code, m.name]),
    [methodFeed.data]);
  const hdpStatuses = useMemoHdp(
    () => (statusFeed.data || []).map(x => [Number(x.id), x.label, HDP_STATUS_CLASS[Number(x.id)] || "hdp-chip--unknown"]),
    [statusFeed.data]);

  const [f, setF] = useStateHdp(HDP_DEFAULT_FILTERS);
  const set = (patch) => setF(s => ({ ...s, ...patch }));
  const [sort, setSort] = useStateHdp({ key: "id", dir: "desc" }); // real default: ID desc
  const [pageSize, setPageSize] = useStateHdp(100); // real default page length
  const [page, setPage] = useStateHdp(0);
  const [modal, setModal] = useStateHdp(null); // { mode, row }
  const [busy, setBusy] = useStateHdp(false);
  const [sheetOpen, setSheetOpen] = useStateHdp(false);
  const [expanded, setExpanded] = useStateHdp({}); // mobile card expand

  /* Sidebar-badge poll, simulated visually: the real admin re-polls
     GET /getPendingCounts every 3 s (setInterval(pipe, 3000) in main.js, READ
     UNCOMMITTED server-side); when the combined pending total increases the
     browser plays #notificationSound, opens a notification modal and prefixes
     the tab title with (N). Here the tick only restarts the pulse dot —
     no invented data. */
  const [pollTick, setPollTick] = useStateHdp(0);
  useEffectHdp(() => { const t = setInterval(() => setPollTick(x => x + 1), 3000); return () => clearInterval(t); }, []);
  const pendingCount = useMemoHdp(() => rows.filter(r => r.status === 0).length, [rows]); // countPendingDeposits: payment_status=0 only — method NOT filtered, so 'online' rows count too

  /* ---- filtering (mirrors the server-side column searches) ---- */
  /* The autocomplete list is the set of players who actually appear in the
     fetched requests — not a fixed roster. "Known" therefore means "has a
     deposit in this window", which is what the filter can match on. */
  const hdpUsers = useMemoHdp(
    () => Array.from(new Set(rows.map(r => r.user).filter(Boolean))).sort(),
    [rows]);
  const knownUser = f.user.trim() && hdpUsers.some(u => u.toLowerCase() === f.user.trim().toLowerCase());
  const filtered = useMemoHdp(() => {
    const needle = f.payDetails.trim().toLowerCase();
    const from = parseFloat(f.amtFrom); const to = parseFloat(f.amtTo);
    const dFrom = f.dateFrom ? new Date(f.dateFrom + "T00:00:00").getTime() : null;
    const dTo = f.dateTo ? new Date(f.dateTo + "T23:59:59").getTime() : null;
    const uSel = f.user.trim().toLowerCase();
    return rows.filter(r => {
      if (f.method && r.method !== f.method) return false;
      if (f.status !== "" && r.status !== Number(f.status)) return false; // server correctly uses !== "" so Pending(0) filters
      // Real server wraps amount bounds in !empty(), so a typed bound of 0 is
      // silently ignored there; a 0 bound is meaningless for deposits, so the
      // prototype just parses numbers.
      if (!isNaN(from) && r.amount < from) return false;
      if (!isNaN(to) && r.amount > to) return false;
      if (dFrom && r.ts < dFrom) return false;
      if (dTo && r.ts > dTo) return false;
      if (uSel) {
        const isSelf = r.user.toLowerCase() === uSel;
        const isChild = (r.parent || "").toLowerCase() === uSel;
        // switch default = single account (exact user_path match)
        if (f.searchType === "all") { if (!isSelf && !isChild) return false; }
        else if (f.searchType === "subaccounts") { if (!isChild) return false; }
        else { if (!isSelf) return false; }
      }
      if (needle) {
        const hay = [r.cuit, r.cvi, r.recipient].filter(Boolean).join(" ").toLowerCase(); // sender_name NOT searched — real behavior
        if (!hay.includes(needle)) return false;
      }
      return true;
    });
  }, [rows, f]);
  // <!-- SUGGESTION: the server also honours an exact-match column search on ordini.id, but the real form renders no ID input — expose it. -->

  /* ---- sorting (server whitelist $columnMap; unknown keys fall back to id) ---- */
  const sorted = useMemoHdp(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    const val = (r) => {
      switch (sort.key) {
        case "id": return r.id;
        case "parent": return r.parent || "";
        case "user": return r.user;
        case "ts": return r.ts;
        case "amount": return r.amount;
        case "method": return hdpMethodLabel(r.method);
        case "status": return r.status;
        /* Real $columnMap maps "Last transaction" to ordini.addedTime — it sorts by
           the row's OWN date, not the computed last-transaction value. Prototype
           sorts by the actual last-approved-transaction timestamp (evident intent). */
        case "lasttx": return r.lastTx ? r.lastTx.ts : 0;
        case "reason": return r.reason || "";
        default: return r.id;
      }
    };
    return [...filtered].sort((a, b) => { const x = val(a), y = val(b); return (x < y ? -1 : x > y ? 1 : 0) * dir; });
  }, [filtered, sort]);
  // <!-- SUGGESTION: sort the Last-transaction column by the computed last transaction server-side (or drop sorting on a per-row N+1 computed column). -->

  useEffectHdp(() => { setPage(0); }, [f, pageSize]);
  const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
  const pageRows = sorted.slice(page * pageSize, page * pageSize + pageSize);

  const onSort = (key) => setSort(s => s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "id" || key === "ts" || key === "amount" ? "desc" : "asc" });
  const SortTh = ({ k, children, style }) => (
    <th className={`hdp-th ${sort.key === k ? "active" : ""}`} style={style} onClick={() => onSort(k)} title="Sort">
      {children}
      <span className="hdp-sort-ic"><Icon name={sort.key === k && sort.dir === "asc" ? "arrow_up" : "arrow_down"} size={9} /></span>
    </th>
  );

  /* ---- actions ---- */
  /* Approve — evident intent. REAL-PLATFORM BUG (reference §Deposits Notes): approveDeposit
     fetches the pending order and calls TransferController::processTransfer with NO DB
     transaction / row lock between fetch and transfer — two operators clicking at the same
     time race, and the second save path is guarded only by the initial payment_status=0
     fetch. The prototype implements the evident intent: the confirm button hard-disables
     while the transfer is in flight and the handler ignores re-entry, so a double-click
     cannot double-submit. */
  // <!-- SUGGESTION: wrap fetch → processTransfer → save in a DB transaction with SELECT … FOR UPDATE on the ordini row so concurrent approvals cannot double-credit. -->
  const confirmAction = async (reason) => {
    if (busy || save.busy || !modal) return; // re-entry guard (see race note above)
    const { mode, row } = modal;
    if (row.status !== 0) { setModal(null); return; }
    setModal(null);
    setBusy(true);
    /* APPROVE MOVES MONEY. sb.approveDeposit posts through post_transaction()
       first and only then marks the request, attaching the ledger entry that
       settled it — the table's CHECK refuses status 1 without one, so the wrong
       order fails loudly rather than leaving a request that claims to be paid.

       The idempotency key is `deposit:<id>:approve`, derived from the request,
       so the double-click this screen's own race note describes posts the SAME
       key and post_transaction() returns the first entry instead of crediting
       twice. That is the fix for the real platform's unguarded
       fetch-then-transfer, and it lives in the database rather than in a
       disabled button. */
    if (mode === "approve") {
      await save.run(() => window.sb.approveDeposit({
        requestId: row.id,
        userId: row.userId,
        amount: row.amount,
        description: `Deposit request ${row.id}`,
      }), {
        done: `Deposit #${row.id} approved — ${hdpAmount(row.amount, row.currency)} credited to ${row.user}`,
        fail: `Deposit #${row.id} was NOT approved`,
      });
    } else {
      /* A rejection moves no money: status and a reason, nothing else. The
         table's other CHECK stops anyone attaching a ledger entry to one. */
      await save.run(() => window.sb.rejectDeposit({ requestId: row.id, reason }), {
        done: `Deposit #${row.id} rejected`,
        fail: `Deposit #${row.id} was NOT rejected`,
      });
    }
    setBusy(false);
  };

  /* Export — evident intent. REAL-PLATFORM BUG (reference §Deposits Export): the XLSX
     export re-runs the query AFTER offset()/limit() were applied, so it exports only the
     current page (≤ page length rows), and all generation exceptions are swallowed into an
     empty download link. The prototype exports EVERY filtered row. Real output:
     Deposits-<timestamp>-<admin id>.xlsx via PHPSpreadsheet; the demo ships CSV through the
     shared engine downloader. Column set matches the real export (Currency as own column,
     status text without tags, wire details concatenated). */
  // <!-- SUGGESTION: build the export query before offset/limit so it covers the full filtered set, and surface generation errors instead of returning a silent empty link. -->
  const doExport = () => {
    if (!window.PAYBO?.downloadCSV) return hdpToast("Export");
    const stamp = Math.floor(Date.now() / 1000);
    window.PAYBO.downloadCSV(`Deposits-${stamp}-hostadmin.csv`, sorted, [
      { key: "id", label: "ID" },
      { key: "user", label: "Username" },
      { key: "parent", label: "Parent", get: (r) => r.parent || "-" },
      { key: "amount", label: "Amount", get: (r) => Number(r.amount).toFixed(2) },
      { key: "currency", label: "Currency" },
      { key: "ts", label: "Date", get: (r) => hdpDate(r.ts) },
      { key: "method", label: "Method", get: (r) => hdpMethodLabel(r.method) },
      { key: "status", label: "Payment status", get: (r) => hdpStatusOf(r)[0] },
      { key: "details", label: "Payment details", get: (r) => r.method === "wire-argentina" ? `Nombre: ${r.sender} , Destinatario: ${r.recipient} , CVU/CBU: ${r.cvi}` : "-" },
      { key: "lasttx", label: "Last transaction", get: (r) => r.lastTx ? `${hdpAmount(r.lastTx.amount, r.lastTx.currency)} ${hdpDate(r.lastTx.ts)}` : "-" },
      { key: "reason", label: "Decline reason", get: (r) => (r.status === 2 && r.reason) || "-" },
    ]);
    hdpToast(`Exported ${sorted.length} filtered rows`);
  };

  const activeFilters = Object.keys(HDP_DEFAULT_FILTERS).filter(k => f[k] !== HDP_DEFAULT_FILTERS[k]);
  const clearAll = () => setF(HDP_DEFAULT_FILTERS);
  const methodName = (code) => (hdpMethods.find(m => m[0] === code) || [code, hdpMethodLabel(code)])[1];

  const openApprove = (r) => setModal({ mode: "approve", row: r });
  const openDecline = (r) => setModal({ mode: "decline", row: r });

  return (
    <div className="page report-page host-players host-fm hdp-page">
      <div className="page__header" style={{ justifyContent: "space-between", width: "100%", flexWrap: "wrap", gap: 12 }}>
        <div>
          <div className="page__title" style={{ color: "var(--p-700)", display: "inline-flex", alignItems: "center" }}>
            Deposits
            <Tip>
              Sidebar entry gated by <code>checkUserBoPerm('support_deposits')</code> inside the role gate <code>isadmin() || isSkinAdmin() || isAdministration() || isCustomCare() || isShop()</code>. Only AFFILIATE, CUSTOMER_CARE and ADMINISTRATION are actually checked against the permissions table — every other listed role passes automatically. The gate is <b>sidebar-visibility only</b>: the /deposits routes enforce just the <code>['auth','admin','2fa','g2fa']</code> middleware plus hierarchy scoping (<code>users.user_path LIKE '&lt;caller path&gt;/%'</code>).
            </Tip>
          </div>
          <div className="page__subtitle">Deposit requests across your network — review, approve or reject pending transfers.</div>
        </div>
        <div className="page__actions" style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
          <div className="hdp-poll" title="Sidebar pending badge — re-polled every 3 seconds">
            <span key={pollTick} className="hdp-poll-dot" />
            <b>{pendingCount}</b>&nbsp;pending
            <span className="hdp-poll-sub">auto-poll 3s</span>
            <Tip>The sidebar "Deposits" badge re-polls <code>GET /getPendingCounts</code> every 3 seconds (READ UNCOMMITTED). When the combined pending total increases, the admin plays a notification sound, opens a notification modal and prefixes the tab title with (N). Online (PSP-processed) deposits count toward the badge even though they cannot be manually approved here.</Tip>
          </div>
          <button className="hdp-filters-btn" onClick={() => setSheetOpen(true)}>
            <Icon name="filter" size={13} /> Filters{activeFilters.length > 0 && <span className="hdp-fcount">{activeFilters.length}</span>}
          </button>
          <button className="rpt-btn rpt-btn--export" style={{ minWidth: 0, height: 38 }} onClick={doExport}><Icon name="download" size={13} /> Export</button>
        </div>
      </div>

      <Explainer compact title="What this queue is, in plain English"
        bullets={[
          <><b>Approve</b> credits the player through an internal transfer (description "Deposit #id"), then runs promo-code redemption, deposit bonuses and legacy promotions — reward failures are logged, never blocking the approval.</>,
          <><b>Reject</b> moves no money — it marks the request Rejected and stores your reason for the player.</>,
          <><b>Online</b> rows are PSP-processed: no manual approve/reject here, but they still count toward the pending badge.</>,
        ]}>
        Manual deposit requests submitted by players (bank wire, transferencia, Cripten). Actions appear only on <b>Pending</b> rows; processed rows show who handled them and when.
      </Explainer>

      {/* --- Hero filter strip (desktop) — Transactions.jsx shape --- */}
      <div className="hdp-hero">
        <HdpFilterControls f={f} set={set} sheet={false} idSuffix="hero" methods={hdpMethods} statuses={hdpStatuses} users={hdpUsers} />
        <div className="hdp-fcard hdp-fcard--result">
          <div className="filter-hero__label"><Icon name="chart" size={11} /> Results</div>
          <div className="filter-hero__value">{sorted.length.toLocaleString()}<span className="filter-hero__value-sub">of {rows.length.toLocaleString()}</span></div>
        </div>
      </div>

      {/* --- Active filter pills --- */}
      {activeFilters.length > 0 && (
        <div className="hdp-pills">
          <span className="hdp-pills-label">Active:</span>
          {f.method && <HdpPill label={methodName(f.method)} onClear={() => set({ method: "" })} />}
          {f.status !== "" && <HdpPill label={hdpStatusLabel(hdpStatuses, f.status)} onClear={() => set({ status: "" })} />}
          {(f.amtFrom !== "" || f.amtTo !== "") && <HdpPill label={`${f.amtFrom || "0"} — ${f.amtTo || "∞"}`} onClear={() => set({ amtFrom: "", amtTo: "" })} />}
          {(f.dateFrom || f.dateTo) && <HdpPill label={`${f.dateFrom || "…"} → ${f.dateTo || "…"}`} onClear={() => set({ dateFrom: "", dateTo: "" })} />}
          {f.user.trim() && <HdpPill label={`${f.user}${f.searchType === "all" ? " + subs" : f.searchType === "subaccounts" ? " (subs)" : ""}${knownUser ? "" : " (no such user)"}`} onClear={() => set({ user: "", searchType: "" })} />}
          {f.payDetails.trim() && <HdpPill label={`Details: “${f.payDetails}”`} onClear={() => set({ payDetails: "" })} />}
          <button className="hdp-clearall" onClick={clearAll}>Clear all</button>
        </div>
      )}

      {/* A failed read and an empty queue look identical in a table body, and
          on a payments queue they mean opposite things: "nothing to approve"
          versus "you cannot see what there is to approve". */}
      {feed.error && <HrsError error={feed.error} onRetry={feed.retry} />}
      {feed.loading && <HrsSkeleton rows={10} cols={8} />}
      {!feed.loading && !feed.error && (
      <div className="panel" style={{ overflow: "hidden" }}>
        {/* --- Desktop table --- */}
        <div className="hdp-tablewrap">
          <table className="data-table hp-list fm-table hdp-table">
            <thead>
              <tr>
                <SortTh k="id" style={{ width: 72 }}>ID</SortTh>
                <SortTh k="parent">Parent</SortTh>
                <SortTh k="user">Username</SortTh>
                <SortTh k="ts">Date</SortTh>
                <SortTh k="amount">Amount</SortTh>
                <SortTh k="method">Method</SortTh>
                <SortTh k="status">Payment status</SortTh>
                <th>Payment details</th>
                <SortTh k="lasttx">Last transaction</SortTh>
                <SortTh k="reason">Decline reason</SortTh>
                <th style={{ width: 92 }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {pageRows.length === 0 && (
                <tr><td colSpan={11} className="hdp-empty">
                  <Icon name="search" size={20} style={{ opacity: .4 }} />
                  <div className="hdp-empty-t">No deposit requests match your filters</div>
                  <div className="hdp-empty-s">Try clearing a filter or widening the date range.</div>
                </td></tr>
              )}
              {pageRows.map(r => (
                <tr key={r.id} className={r.status === 0 ? "hdp-row--pending" : ""}>
                  <td><CopyableId value={r.id} /></td>
                  <td>{r.parent || "-"}</td>
                  <td className="hdp-user">{r.user}</td>
                  <td className="fm-date">{hdpDate(r.ts)}</td>
                  <td className="hdp-amt">{hdpAmount(r.amount, r.currency)}</td>
                  <td>{hdpMethodLabel(r.method)}</td>
                  <td><HdpStatusCell r={r} /></td>
                  <td><HdpPayDetails r={r} /></td>
                  <td className="fm-last">{r.lastTx ? <>{hdpAmount(r.lastTx.amount, r.lastTx.currency)}<div>{hdpDate(r.lastTx.ts)}</div></> : <span className="hdp-dim">—</span>}</td>
                  <td className="fm-decline">{r.status === 2 && r.reason ? r.reason : <span className="hdp-dim" style={{ fontStyle: "normal" }}>—</span>}</td>
                  <td><HdpRowActions r={r} onApprove={openApprove} onDecline={openDecline} block={busy} /></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        {/* --- Mobile stacked cards (§11: player, amount, status, date scan-first) --- */}
        <div className="hdp-cards">
          {pageRows.length === 0 && (
            <div className="hdp-empty" style={{ padding: 28 }}>
              <div className="hdp-empty-t">No deposit requests match your filters</div>
              <div className="hdp-empty-s">Try clearing a filter or widening the date range.</div>
            </div>
          )}
          {pageRows.map(r => {
            const open = !!expanded[r.id];
            const [label, cls] = hdpStatusOf(r);
            return (
              <div key={r.id} className={`hdp-mcard ${r.status === 0 ? "hdp-mcard--pending" : ""}`}>
                <div className="hdp-mtop">
                  <span className="hdp-muser">{r.user}</span>
                  <span className="hdp-mamt">{hdpAmount(r.amount, r.currency)}</span>
                </div>
                <div className="hdp-msub">
                  <span className={`fm-chip ${cls}`}>{label}</span>
                  <span className="hdp-mdate">{hdpDate(r.ts)}</span>
                  <button className="hdp-mexpand" onClick={() => setExpanded(x => ({ ...x, [r.id]: !open }))}>
                    {open ? "Less" : "Details"} <Icon name={open ? "chevron_down" : "chevron_right"} size={11} />
                  </button>
                </div>
                {open && (
                  <div className="hdp-mbody">
                    <div className="hdp-mkv"><span className="k">ID</span><span className="v">#{r.id}</span></div>
                    <div className="hdp-mkv"><span className="k">Parent</span><span className="v">{r.parent || "-"}</span></div>
                    <div className="hdp-mkv"><span className="k">Method</span><span className="v">{hdpMethodLabel(r.method)}</span></div>
                    {r.status !== 0 && <div className="hdp-mkv"><span className="k">Processed</span><span className="v">{hdpDate(r.updateTs)} · {r.doneBy}</span></div>}
                    {r.method === "wire-argentina" && <HdpPayDetails r={r} />}
                    <div className="hdp-mkv"><span className="k">Last transaction</span><span className="v">{r.lastTx ? `${hdpAmount(r.lastTx.amount, r.lastTx.currency)} · ${hdpDate(r.lastTx.ts)}` : "—"}</span></div>
                    {r.status === 2 && r.reason && <div className="hdp-mkv"><span className="k">Decline reason</span><span className="v" style={{ color: "#e9484a" }}>{r.reason}</span></div>}
                    {r.status === 0 && r.method !== "online" && (
                      <div className="hdp-mact">
                        <button style={{ background: "#1f9d57" }} disabled={busy} onClick={() => openApprove(r)}><Icon name="check" size={14} /> Approve</button>
                        <button style={{ background: "#e9484a" }} disabled={busy} onClick={() => openDecline(r)}><Icon name="x" size={14} /> Reject</button>
                      </div>
                    )}
                    {r.status === 0 && r.method === "online" && (
                      <div className="hdp-monline">Online (PSP-processed) — settled by the gateway, no manual action here.</div>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {/* --- Pagination (real: server-side, default 100, lengthMenu 5/10/25/50/100) --- */}
        <div className="hdp-pager">
          <span>Show
            <select className="select hdp-psize" value={pageSize} onChange={e => setPageSize(Number(e.target.value))}>
              {HDP_PAGE_SIZES.map(n => <option key={n} value={n}>{n}</option>)}
            </select>
            entries
          </span>
          <span className="hdp-pager-range">
            Showing <b>{sorted.length === 0 ? 0 : page * pageSize + 1}–{Math.min(sorted.length, (page + 1) * pageSize)}</b> of <b>{sorted.length.toLocaleString()}</b>
            <span style={{ marginLeft: 8 }}>Page {page + 1} of {totalPages}</span>
          </span>
          <span className="hdp-pager-btns">
            <button className="btn btn--secondary btn--sm" disabled={page === 0} onClick={() => setPage(p => Math.max(0, p - 1))}><Icon name="chevron_left" size={12} /> Prev</button>
            <button className="btn btn--secondary btn--sm" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next <Icon name="chevron_right" size={12} /></button>
          </span>
        </div>
      </div>
      )}

      {/* --- Mobile filter sheet --- */}
      {sheetOpen && (
        <div className="hdp-sheet-scrim" onClick={() => setSheetOpen(false)}>
          <div className="hdp-sheet" onClick={e => e.stopPropagation()}>
            <div className="hdp-sheet-head">
              <span><Icon name="filter" size={14} /> Filters</span>
              <button className="hdp-sheet-x" onClick={() => setSheetOpen(false)}><Icon name="x" size={13} /></button>
            </div>
            <div className="hdp-sheet-body">
              <HdpFilterControls f={f} set={set} sheet={true} idSuffix="sheet" methods={hdpMethods} statuses={hdpStatuses} users={hdpUsers} />
            </div>
            <div className="hdp-sheet-foot">
              <button className="hdp-btn hdp-btn--ghost" onClick={clearAll}>Clear all</button>
              <button className="hdp-btn hdp-btn--ok" style={{ background: "var(--p-600)" }} onClick={() => setSheetOpen(false)}>
                Show {sorted.length.toLocaleString()} results
              </button>
            </div>
          </div>
        </div>
      )}

      {modal && (
        <HdpActionModal mode={modal.mode} row={modal.row} busy={busy}
          onConfirm={confirmAction} onClose={() => setModal(null)} />
      )}
    </div>
  );
};

window.HostDeposits = HostDeposits;
