// Represents: admin.currencies.index · Admin/CurrencyController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Currency exchange rates"
/* ====================================================================
   CURRENCY EXCHANGE RATES — Settings ▾ rebuild (Batch 3), Hrs* shell
   ====================================================================
   Real screen: `GET /currencies` (`admin.currencies.index`, routes/admin.php
   L1664-1669) → Admin/CurrencyController::index L14 / ::rows L35 (DataTables
   JSON) / ::edit L95 / ::store L108. Views: the shared generic table shell
   `admin.generics.index` with `generic_filters_tpl = 'currencies'`
   (generics/filters/currencies.blade.php) + edit-modal body
   generics/models/currency.blade.php, driven by
   public/js/pages/generic/currencies.js. Cross-file sibling used by the
   Update button: `crons.exchange.rates` → GET /crons/downloadExchangeRates
   (routes/cronjobs.php L117 → CronsController::downloadExchangeRates).

   WHAT THE DATA ACTUALLY IS
   - `currencies_list` (DISTINCT currency, auto_update) is the row source —
     the master list of currencies the platform knows.
   - `currencies` is the rate store: one EUR-based rate per currency per day,
     `rate` DECIMAL(15,4), UNIQUE(currency, date_currency).
   - The Rate cell is `CurrencyConverter->exchangeRates[currency]` — the row
     whose `date_currency` is NEAREST to today in EITHER direction
     (ROW_NUMBER() OVER (PARTITION BY currency ORDER BY ABS(DATEDIFF(...)))),
     so a gap is filled by carrying a rate forward OR backward.
   - `exchange_rates` is an append-only audit copy written by the download
     job — `rate` is DOUBLE(8,2) there, i.e. LOWER precision than the source.

   Faithful absences (nothing added — brief §3):
   - NO create: the controller sets `no_create` (L28), so the generic shell
     renders no "New" button. Currencies appear by being in `currencies_list`.
   - NO export: the generic layout loads the DataTables Buttons/pdfmake
     bundles but currencies.js configures no buttons — so no HrsExport here.
     // <!-- SUGGESTION: either configure a real export button or stop loading
     //      the export CDN bundles on this screen — today they ship unused. -->
   - NO KPIs / totals / bulk actions: none exist on the real screen.
   - NO Reset button: the filters template ships Search + Update only. (The
     shell's active-filter pills fall back to per-field clearing.)
   - NO date picker in the edit form: `edit()` accepts `?date=Y-m-d` and
     resolves the nearest rate for it, but the date is never rendered and
     `store()` always writes TODAY's row — historic rates cannot be edited
     from the UI. Represented as the modal's "writes today's row" note
     instead of an invented date field.
     // <!-- SUGGESTION: render the resolved date_currency in the edit form and
     //      let store() honour it — the ?date= plumbing already exists and is
     //      dead weight without it. -->
   - NO rate-history / sparkline column: `currencies` holds a per-day series
     but the screen shows a single number.
     // <!-- SUGGESTION: surface the date_currency the displayed rate actually
     //      came from (and a small per-currency history) — the nearest-date
     //      fallback can silently display a rate carried in from another day
     //      with nothing on screen saying so. -->

   Divergences implemented as evident intent (known-bug policy, CLAUDE.md):
   1. Pagination total — `rows()` sets `iTotalRecords = count($res)` AFTER
      offset/limit (L72), so the real pager's record count is the size of the
      current page whenever more than one page exists. This rebuild pages on
      the true filtered total.
      // <!-- SUGGESTION: count the filtered query before offset/limit so the
      //      DataTables pager stops reporting "50 of 50" on every page. -->
   2. Sorting — currencies.js declares an initial order [[0,"desc"]] and marks
      `actions` non-orderable, but `rows()` ignores the DataTables order
      params entirely and always `orderBy('currency')` ASC (L67): clicking a
      header does nothing on the live platform. Here the three data columns
      sort, with the real server order (Currency ASC) as the default.
      // <!-- SUGGESTION: honour the DataTables order params in rows(), or drop
      //      the sort affordance from the headers so it stops lying. -->

   Real-platform findings surfaced on screen (not fixed, not hidden):
   - The Update button calls an UNAUTHENTICATED endpoint. `GET
     /crons/downloadExchangeRates` sits in the `cronjobs` middleware group,
     which is only `throttle:api` + SubstituteBindings (app/Http/Kernel.php
     L66-70) — no auth, no 2FA, no role check. Anyone who can reach the crons
     domain can queue the rate-download job. Rendered as an honest finding
     card; the button itself IS a real operator-facing control (filters/
     currencies.blade.php L61 + currencies.js L78-88), so it is kept on
     screen — but DISABLED, with the endpoint it needs named in its tooltip.
     Nothing in this prototype dispatches a job or fetches a rate, and a
     "Job queued" toast for work that never happened is worse than an honest
     dead control.
     // <!-- SUGGESTION: move /crons/downloadExchangeRates behind the same
     //      auth/2FA (or a signed-URL / shared-secret) the rest of the admin
     //      uses, and have the screen's Update button call an admin-side
     //      route that dispatches the job. -->
   - `store()` authorizes with `viewAny` — CurrencyPolicy has no `update`
     ability, so view permission IS write permission (both are `isadmin()`).
     // <!-- SUGGESTION: add CurrencyPolicy::update and authorize store()
     //      against it, so read-only super-admin variants stay possible. -->
   - The legacy commented-out downloadExchangeRates() (CurrencyConverter.php
     L68-171) carries a hardcoded apilayer/fixer API key in the comment.
     // <!-- SUGGESTION: rotate that key and delete the commented block — a
     //      committed credential is a credential, comment or not. -->
   - The rate input is `step=0.01` on a DECIMAL(15,4) column, so the spinner
     cannot reach the two extra decimals the column stores.
     // <!-- SUGGESTION: set step=0.0001 to match the column precision. -->
   - The filters template ships a dead copy-pasted #banModal + doBan() block
     (filters/currencies.blade.php L39-58) that nothing on this screen uses.
     // <!-- SUGGESTION: delete the copy-pasted ban modal from the currencies
     //      filter template. -->
   - `CurrencyConverter::$skipped_currencys = ['SDG']` is declared and never
     read anywhere — SDG is NOT actually skipped by active code.

   Label policy (CLAUDE.md): `backend.currency_exchange_rates`, `backend.rate`,
   `backend.auto_update` and `backend.job_queued` resolve in no committed lang
   file (storage/lang is gitignored) — operator-facing wording is written here
   and marked "label inferred". Resolved keys used verbatim: currency
   "Currency", actions "Actions", search_button "Search", update "Update",
   select_option "Select", yes/no "Yes"/"No", operation_ok "Operation
   performed successfully.".

   Demo session = Super admin "admin" (user_level 0) — the only role that
   passes CurrencyPolicy::viewAny, so the Edit action renders.

   All new top-level names are hsc2/Hsc2/HSC2-prefixed except the required
   page component `SetCurrencies` (app.jsx case "settings-currencies"); this
   file loads after the legacy HostSettings.jsx, so this definition wins.
   ==================================================================== */

const { useState: hsc2UseState, useMemo: hsc2UseMemo } = React;

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

/* ---------- deterministic PRNG (mulberry32) — rows render identically
     on every load, like the rest of the prototype's mock data ---------- */
const hsc2Rand = (seed) => () => {
  seed = (seed + 0x6D2B79F5) | 0;
  let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};

/* DECIMAL(15,4) — the column stores 4 decimals and MySQL renders them all;
   no thousands separator, matching the raw value the real cell prints. */
const hsc2Rate4 = (n) => (n == null ? "—" : Number(n).toFixed(4));
const hsc2Round4 = (n) => Math.round(n * 10000) / 10000;

const hsc2Pad = (n) => String(n).padStart(2, "0");
/* store() writes `date('Y-m-d')`; risistemadata() displays d/m/Y. */
const hsc2Iso = (d) => `${d.getFullYear()}-${hsc2Pad(d.getMonth() + 1)}-${hsc2Pad(d.getDate())}`;
const hsc2Dmy = (d) => `${hsc2Pad(d.getDate())}/${hsc2Pad(d.getMonth() + 1)}/${d.getFullYear()}`;
const HSC2_TODAY = new Date(2026, 7, 7); /* demo "today" — the row store() would write */

/* ---------- row source: `currencies_list` (DISTINCT currency, auto_update)
   joined with today's converter rate. Production contents are data, not code
   (docs/ISYSTEM_REFERENCE.md Batch 6 §5: "cannot be enumerated from the
   repo"), so the codes here are the ones the reference actually names — the
   payment-method modal set (EUR/USD/GBP/BRL/TRY/CAD/MXN/INR), the BO symbol
   map addition (JPY), ARS (bluelytics special case), KES (KRA daily iTax),
   SDG (the declared-but-unused skip list) — plus the LATAM/Africa currencies
   the demo skins already use elsewhere in the prototype (PYG, UYU, CLP, COP,
   PEN, BOB, ZAR, NGN, LBP, CHF, AED, DOP, CRC, GTQ).
   Anchors are realistic EUR-based magnitudes; the PRNG applies a small
   deterministic daily drift on top. auto_update mirrors the column default
   (true) with a few operator-pinned rows so the Yes/No filter has both. ---------- */
/* ---------- the row source ----------------------------------------------
   Was a 26-currency table of hand-written anchor rates with deterministic
   ±0.6% drift, so the screen "read like a real nightly download". It read
   like one because it was built to; nothing behind it existed.

   Now: currency_latest_rate (008_screen_views.sql) — the master list from
   `currencies` left-joined to the nearest-dated row in `currency_rates`,
   resolved by ABS(date - today) exactly as isystem does. The view is
   security_invoker, so RLS applies and an unauthenticated caller sees nothing.

   `rate` is NULL for a currency with no quote at all. That is a real state —
   a currency the platform knows about but has never priced — and the screen
   shows it as "—" rather than inventing a number for it. */
const hsc2Row = (r) => ({
  currency:    r.code,
  rate:        r.rate == null ? null : Number(r.rate),
  auto_update: !!r.auto_update,
  rate_date:   r.rate_date,
  rate_age:    r.rate_age_days,
});

/* ---------- Auto update cell: currencies_list.auto_update rendered as
   backend.yes / backend.no ("Yes" / "No") ---------- */
const Hsc2AutoChip = ({ on }) => (
  <span className={`chip ${on ? "chip--ok" : "chip--neutral"}`}>{on ? "Yes" : "No"}</span>
);

/* ---------- table columns (display order per Admin/CurrencyController::rows)
   Currency = computed string 'EUR -> ' . currency (L78) ---------- */
const hsc2Columns = (onEdit) => [
  {
    key: "currency", label: "Currency", sortable: true, firstDir: "asc", width: 190,
    render: (row) => (
      <span className="hsc2-pair">
        <span className="hsc2-base">EUR</span>
        <span className="hsc2-arrow">&rarr;</span>
        <span className="hsc2-code">{row.currency}</span>
      </span>
    ),
  },
  {
    /* label inferred — backend.rate resolves in no committed lang file */
    key: "rate", label: "Rate", align: "right", sortable: true, firstDir: "desc", width: 160,
    render: (row) => <span className="hsc2-rate">{hsc2Rate4(row.rate)}</span>,
  },
  {
    /* label inferred — backend.auto_update resolves in no committed lang file */
    key: "auto_update", label: "Auto update", align: "center", sortable: true, width: 130,
    render: (row) => <Hsc2AutoChip on={row.auto_update} />,
  },
  {
    /* Client-side visibility only on the real screen (currencies.js L36-41
       renders the pencil when user.is_admin == 1); the actual enforcement is
       CurrencyPolicy on edit/store. */
    key: "actions", label: "Actions", align: "center", width: 90,
    render: (row) => (
      <button className="hsc2-editbtn" title="Edit" onClick={() => onEdit(row)}>
        <Icon name="edit" size={13} />
      </button>
    ),
  },
];

/* ---------- filters (generics/filters/currencies.blade.php)
   Auto update select → yes/no mapped server-side to 1/0 equality;
   Currency text → `currency LIKE %value%`. No Reset button exists. ---------- */
const HSC2_FIELDS = [
  {
    key: "auto_update", label: "Auto update", type: "select", icon: "refresh",
    placeholder: "Select", /* backend.select_option = "Select" */
    options: [{ value: "yes", label: "Yes" }, { value: "no", label: "No" }],
    tip: <>Maps to <code>currencies_list.auto_update</code> — <b>yes</b> → 1, <b>no</b> → 0. Rows with auto-update off keep whatever rate an operator last saved; the nightly job skips them entirely.</>,
  },
  {
    key: "currency", label: "Currency", type: "text", icon: "globe", grow: true, placeholder: "Currency",
    tip: <>Server-side <code>currency LIKE %value%</code> — a substring match on the ISO code, not an exact lookup.</>,
  },
];

/* ==================================================================
   Edit modal — generics/models/currency.blade.php inside the generic
   generaModalGestione() shell. Title = the currency code. Two fields
   only: rate + auto_update. Save posts to admin.currencies.store.
   ================================================================== */
const Hsc2EditModal = ({ row, onClose, onSave }) => {
  const [rate, setRate] = hsc2UseState(hsc2Rate4(row.rate));
  const [auto, setAuto] = hsc2UseState(!!row.auto_update);
  const [err, setErr] = hsc2UseState("");

  /* Inline Validator::make in store() (L112-115): rate → required|gt:0,
     auto_update → in:on,off. Failure returns ajaxError JSON whose
     `campierrati` keys paint the message under the field. */
  const submit = () => {
    const raw = String(rate).trim();
    if (raw === "") { setErr("The rate field is required."); return; }
    const n = Number(raw);
    if (!Number.isFinite(n) || n <= 0) { setErr("The rate must be greater than 0."); return; }
    onSave({ currency: row.currency, rate: hsc2Round4(n), auto_update: auto });
  };

  return (
    <div className="hsc2-modal-scrim" onClick={onClose}>
      <div className="hsc2-modal" onClick={(e) => e.stopPropagation()}>
        <div className="hsc2-modal__head">
          <div>
            <div className="hsc2-modal__title">{row.currency}</div>
            <div className="hsc2-modal__sub">EUR &rarr; {row.currency} · rate stored as <code>DECIMAL(15,4)</code></div>
          </div>
          <button className="hrs-x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>

        <div className="hsc2-modal__body">
          <div className="hsc2-field">
            <label className="hsc2-label" htmlFor="hsc2-rate">
              Rate {/* label inferred */}
              <Tip size={12}>How many <b>{row.currency}</b> one euro buys. Every conversion in the platform goes through EUR: <code>(amount / rate[from]) * rate[to]</code>.</Tip>
            </label>
            <input
              id="hsc2-rate" className={`hsc2-input${err ? " hsc2-input--err" : ""}`}
              type="number" min="0.01" step="0.01" lang="en"
              value={rate} onChange={(e) => { setRate(e.target.value); setErr(""); }}
            />
            {err && <div className="hsc2-err"><Icon name="alert" size={11} /> {err}</div>}
            {/* Real input ships step=0.01 against a DECIMAL(15,4) column — see header finding. */}
            <div className="hsc2-hint">The spinner steps in 0.01 although the column keeps 4 decimals; type the full precision to use it.</div>
          </div>

          <div className="hsc2-field">
            <label className="hsc2-label">
              Auto update {/* label inferred */}
              <Tip size={12}>When on, the nightly job overwrites this rate from the upstream feed. When off, the currency is skipped by the job and keeps whatever an operator last saved here.</Tip>
            </label>
            <Toggle value={auto} onChange={setAuto} onLabel="Yes" offLabel="No" />
          </div>

          <div className="hsc2-note">
            <Icon name="info" size={12} />
            <span>
              Saving writes <b>today's</b> row — <code>Currency::updateOrCreate(['currency' =&gt; '{row.currency}', 'date_currency' =&gt; '{hsc2Iso(HSC2_TODAY)}'], ['rate' =&gt; …])</code> — and updates <code>currencies_list.auto_update</code>.
              A rate already stored for {hsc2Dmy(HSC2_TODAY)} is overwritten; earlier days are never touched and cannot be edited from this screen.
            </span>
          </div>
        </div>

        <div className="hsc2-modal__foot">
          <button className="hrs-btn hrs-btn--reset" onClick={onClose}>Close</button>
          <button className="hrs-btn hrs-btn--search" onClick={submit}><Icon name="check" size={14} /> Save</button>
        </div>
      </div>
    </div>
  );
};

/* ==================================================================
   Automatic rate sync — documentation of the real pipeline, plus the
   unauthenticated-endpoint finding. Describes behavior; it does not
   fabricate live sync status the real screen never shows.
   ================================================================== */
const Hsc2SyncPanel = () => (
  <div className="hsc2-sync">
    <div className="hsc2-sync__grid">
      <div className="hsc2-sync__item">
        <div className="hsc2-sync__k"><Icon name="calendar" size={11} /> Schedule</div>
        <div className="hsc2-sync__v">3× nightly</div>
        <div className="hsc2-steps">
          <span className="hsc2-step">00:01</span>
          <span className="hsc2-step">00:06</span>
          <span className="hsc2-step">00:12</span>
        </div>
        <div className="hsc2-sync__s">
          <code>DownloadExchangeRatesJob</code> is scheduled three times (Console/Kernel.php L70-73). The extra two are retries, not extra downloads:
          the job is <code>ShouldBeUnique</code> with a <b>static</b> <code>uniqueId()</code>, so a run still in flight swallows the next tick.
        </div>
      </div>

      <div className="hsc2-sync__item">
        <div className="hsc2-sync__k"><Icon name="globe" size={11} /> Source</div>
        <div className="hsc2-sync__v">TimelessTech</div>
        <div className="hsc2-sync__s">
          <code>GET {"{host}"}/api/generic/currency/latest</code>, EUR-based. The job reads <code>currencies_list</code> where <code>auto_update = 1</code> in
          chunks of 10, upserts <code>currencies</code> on <code>(currency, date_currency)</code>, and appends the same rows to <code>exchange_rates</code> —
          whose <code>rate</code> is <code>DOUBLE(8,2)</code>, i.e. the audit copy is <b>less precise</b> than the record it audits, and nothing in the app reads it back.
        </div>
      </div>

      <div className="hsc2-sync__item">
        <div className="hsc2-sync__k"><Icon name="flag" size={11} /> ARS exception</div>
        <div className="hsc2-sync__v">bluelytics</div>
        <div className="hsc2-sync__s">
          ARS bypasses the job's numeric/symbol checks and is always re-fetched from <code>api.bluelytics.com.ar/v2/latest</code> →
          <code>blue_euro.value_sell</code> — the Argentine <b>"blue"</b> parallel rate, deliberately not the official one. Every ARS figure converted
          anywhere in the backoffice inherits that choice.
        </div>
      </div>
    </div>

    {/* Real finding, surfaced rather than quietly re-implemented — see header. */}
    <div className="hsc2-finding">
      <div className="hsc2-finding__ic"><Icon name="shield" size={14} /></div>
      <div>
        <div className="hsc2-finding__t">The Update button calls an unauthenticated endpoint</div>
        <div className="hsc2-finding__b">
          <b>Update</b> is a plain <code>$.get(route('crons.exchange.rates'))</code> to <code>GET /crons/downloadExchangeRates</code>. That route lives in the
          <code>cronjobs</code> middleware group, which is <b>only</b> <code>throttle:api</code> + <code>SubstituteBindings</code> (app/Http/Kernel.php L66-70) —
          no auth, no 2FA, no role check. Anyone who can reach the crons domain can queue the rate-download job, on any schedule they like.
          Rate-limiting is the only thing standing in the way.
        </div>
      </div>
    </div>
  </div>
);

/* ==================================================================
   Page component — name required by app.jsx ("settings-currencies").
   Loads after HostSettings.jsx's legacy stub, so this definition wins.
   ================================================================== */
const SetCurrencies = () => {
  window.useLocale && window.useLocale();

  /* One fetch, no local seeding. HrsAsync below renders loading / error /
     empty from the same state — including "not signed in", which is the state
     a table full of invented rows can never show. */
  const feed = useHrsFetch(() => window.sb.list("currencies", { limit: 200 }), []);
  const rows = hsc2UseMemo(() => (feed.data || []).map(hsc2Row), [feed.data]);
  /* Apply-on-Search: the real Search button triggers the DataTables redraw. */
  const [draft, setDraft] = hsc2UseState({ auto_update: "", currency: "" });
  const [applied, setApplied] = hsc2UseState({ auto_update: "", currency: "" });
  /* Server order is always currency ASC (rows() L67) — that is the default here. */
  const [sort, setSort] = hsc2UseState({ key: "currency", dir: "asc" });
  const [page, setPage] = hsc2UseState(0);
  /* currencies.js: pageLength 50, lengthMenu [5,10,25,50]. */
  const [pageSize, setPageSize] = hsc2UseState(50);
  const [editing, setEditing] = hsc2UseState(null);

  const filtered = hsc2UseMemo(() => rows.filter((r) => {
    if (applied.auto_update === "yes" && !r.auto_update) return false;
    if (applied.auto_update === "no" && r.auto_update) return false;
    /* LIKE %value% under a case-insensitive collation */
    const q = (applied.currency || "").trim().toLowerCase();
    if (q && !r.currency.toLowerCase().includes(q)) return false;
    return true;
  }), [rows, applied]);

  const sorted = hsc2UseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    return [...filtered].sort((a, b) => {
      if (sort.key === "rate") return (a.rate - b.rate) * dir;
      if (sort.key === "auto_update") return ((a.auto_update ? 1 : 0) - (b.auto_update ? 1 : 0)) * dir || a.currency.localeCompare(b.currency);
      return a.currency.localeCompare(b.currency) * dir;
    });
  }, [filtered, sort]);

  const view = sorted.slice(page * pageSize, (page + 1) * pageSize);

  /* No write path exists yet: src/supabase.js is read-only by design and a
     rate change is a real edit to shared reference data. Saving therefore says
     so instead of updating a local copy that vanishes on reload and looks like
     it worked. Wiring this is stage 7 of docs/WORK_PLAN.md. */
  const saveRow = ({ currency, rate, auto_update }) => {
    setEditing(null);
    /* backend.operation_ok = "Operation performed successfully." */
    hsc2Toast("Not saved — no write path yet.", `EUR → ${currency} would be written as ${hsc2Rate4(rate)} for ${hsc2Dmy(HSC2_TODAY)} with auto update ${auto_update ? "Yes" : "No"}. Reads are live; writes land in stage 7.`);
  };

  /* Update queues nothing here. On the platform it is
     `$.get(route('crons.exchange.rates'))` → GET /crons/downloadExchangeRates →
     CronsController::downloadExchangeRates, which dispatches
     DownloadExchangeRatesJob and answers backend.job_queued. There is no queue,
     no job and no upstream feed in this prototype, so the button is rendered
     disabled with the endpoint named instead of firing a "Job queued" toast
     that nothing backs. The button stays on screen because the real screen has
     it (filters/currencies.blade.php L61) — and because the unauthenticated
     endpoint it calls is a finding this page is meant to surface, below. */

  return (
    <HrsShell
      title={<>Currency Exchange Rates{/* label inferred — backend.currency_exchange_rates */}</>}
      subtitle="One EUR-based rate per currency per day. Every converted figure in the backoffice — cumulated reports, commissions, the frontend API — is computed from this table."
      gate={<>
        No <code>checkUserBoPerm</code> gate: the sidebar item sits inside <code>@if (isadmin())</code> and every controller action calls{" "}
        <code>authorize('viewAny', new Currency)</code> → <code>CurrencyPolicy::viewAny()</code> = <code>isadmin()</code> — Super Admin (user_level 0) only.{" "}
      </>}
      gateNote={<>
        Unusually for Batch 3, this one <b>is</b> enforced server-side on every action, not just on <code>index</code>. Two caveats: the policy is not registered
        in <code>AuthServiceProvider::$policies</code> (it resolves through Laravel's model→policy auto-discovery, so renaming either class silently removes the
        gate), and <code>store()</code> authorizes with <code>viewAny</code> because the policy has no <code>update</code> ability — view permission is write
        permission. The Edit pencil itself is only hidden client-side via <code>user.is_admin == 1</code>.
      </>}
      explainer={{
        title: "What this screen is, in plain English",
        bullets: [
          <>Each row is a currency the platform knows (<code>currencies_list</code>) shown with <b>today's</b> rate from <code>currencies</code> — the number of units of that currency one euro buys. EUR is the base, so <code>EUR → EUR</code> is 1.0000.</>,
          <><b>Nearest-date fallback, in both directions.</b> The rate shown is the row whose <code>date_currency</code> is closest to today by absolute difference — so a missing day is filled by carrying a rate <i>forward</i> or <i>backward</i>. A weekend gap, or a rate you save today, can therefore change how <i>yesterday</i> converts.</>,
          <><b>Conversion is always via EUR:</b> <code>(amount / rate[from]) * rate[to]</code>. That is what every report's "Cumulable" switch runs, what commissions and the KRA daily iTax report use, and what the frontend API hands out at <code>Fapi\CurrencyController::index</code>.</>,
          <><b>Editing writes today only.</b> The form has exactly two fields — rate and auto update — and the save always targets today's <code>(currency, date_currency)</code> row. Historic rates are readable by the code but not editable from here.</>,
          <><b>Auto update</b> decides whether the nightly job overwrites your value: with it on, anything you type here is replaced at 00:01. Turn it off for a currency you want pinned by hand.</>,
        ],
      }}
      actions={
        /* On the real screen this green download button sits in the filter row
           next to Search (filters/currencies.blade.php L61, currencies.js L78-88);
           moved into the header slot here so the filter strip stays scannable.
           Disabled — see the Update-button comment above saveRow/filtered. */
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
          <button
            className="hrs-btn hrs-btn--export"
            disabled
            aria-disabled="true"
            style={{ opacity: .5, cursor: "not-allowed" }}
            title="Requires backend: GET /crons/downloadExchangeRates (crons.exchange.rates) → dispatches DownloadExchangeRatesJob"
          >
            <Icon name="download" size={14} /> Update
          </button>
          <Tip size={12}>
            Not wired in the prototype — there is no queue worker and no upstream feed here, so it would refresh nothing.
            Requires backend: <code>GET /crons/downloadExchangeRates</code> (<code>crons.exchange.rates</code>) →{" "}
            <code>CronsController::downloadExchangeRates</code> → <code>DownloadExchangeRatesJob</code>. Note that route is
            <b> unauthenticated</b> today — see “Automatic rate sync” below.
          </Tip>
        </span>
      }
    >
      {/* No HrsKpis / HrsBars: the real screen shows no KPIs or totals. */}
      <HrsFilters
        fields={HSC2_FIELDS}
        values={draft}
        onChange={(k, v) => setDraft((d) => ({ ...d, [k]: v }))}
        onSearch={(v) => { setApplied(v); setDraft(v); setPage(0); }}
        /* No onReset — the real filters template ships Search + Update only. */
        resultLabel={`${filtered.length} of ${rows.length}`}
      />

      {/* HrsAsync owns loading, error and empty. The error branch matters most:
          signed out, RLS returns zero rows and a bare table would say "no
          currencies" — which is wrong and unactionable. sb.list surfaces the
          real reason and HrsError prints it. */}
      <HrsAsync
        state={feed}
        skeletonRows={8}
        skeletonCols={4}
        empty="No currencies configured yet. They are seeded by 007_operations.sql."
      >
        {() => (<>
      <HrsTable
        columns={hsc2Columns(setEditing)}
        rows={view}
        sort={sort}
        onSort={(next) => { setSort(next); setPage(0); }}
        rowKey="currency"
        empty="No currency matches these filters."
        renderCard={(r) => (
          <>
            <div className="hrs-card__top">
              <b>EUR &rarr; {r.currency}</b>
              <span className="hsc2-rate">{hsc2Rate4(r.rate)}</span>
            </div>
            <div className="hrs-card__grid">
              <span>Auto update{/* label inferred */}</span><b><Hsc2AutoChip on={r.auto_update} /></b>
            </div>
            <div className="hsc2-cardacts">
              <button className="hsc2-editbtn hsc2-editbtn--wide" onClick={() => setEditing(r)}>
                <Icon name="edit" size={12} /> Edit
              </button>
            </div>
          </>
        )}
      />

      {/* Pager totals use the true filtered count — see header divergence 1. */}
      <HrsPager
        page={page}
        pageSize={pageSize}
        total={filtered.length}
        onPage={setPage}
        onPageSize={(n) => { setPageSize(n); setPage(0); }}
        sizes={[5, 10, 25, 50]}
      />
        </>)}
      </HrsAsync>

      {/* No HrsExport: no export exists on the real screen — see header. */}

      <HrsSection
        title="Automatic rate sync"
        sub="How the rates above get there — the real pipeline behind the Update button."
      >
        <Hsc2SyncPanel />
      </HrsSection>

      {editing && <Hsc2EditModal row={editing} onClose={() => setEditing(null)} onSave={saveRow} />}
    </HrsShell>
  );
};

/* Explicit global — overrides the legacy HostSettings.jsx stub (load order). */
window.SetCurrencies = SetCurrencies;
