/* global React, ReactDOM */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ─────────────────────────────────────────────────────────────
   Localization (RU / KK) — see public/i18n.js
   ───────────────────────────────────────────────────────────── */
const KPOS_LANG = window.KPOS_LANG || 'ru';
const KK = {
  // billing
  'Биллинг': 'Биллинг',
  'Финансы': 'Қаржы',
  'Админка': 'Әкімші панелі',
  'и тариф.': 'және тариф.',
  'Баланс пополняется через Kaspi. Комиссия списывается с каждого успешного платежа.':
    'Баланс Kaspi арқылы толтырылады. Комиссия әрбір сәтті төлемнен есептеп шығарылады.',
  'Пополнить': 'Толтыру',
  'Баланс': 'Баланс',
  'приём приостановлен': 'қабылдау тоқтатылды',
  'низкий баланс': 'төмен баланс',
  'активен': 'белсенді',
  'Текущий баланс': 'Ағымдағы баланс',
  'Баланс в минусе — пополните, чтобы принимать платежи':
    'Баланс минуста — төлем қабылдау үшін толтырыңыз',
  'Низкий баланс — рекомендуем пополнить': 'Төмен баланс — толтыруды ұсынамыз',
  'Достаточно для приёма платежей': 'Төлем қабылдауға жеткілікті',
  'Накопленный оборот': 'Жинақталған айналым',
  'текущая ставка': 'ағымдағы мөлшерлеме',
  'последнее списание': 'соңғы есептен шығару',
  'Тарифные уровни': 'Тариф деңгейлері',
  'ставка по обороту': 'айналым бойынша мөлшерлеме',
  'Оборот': 'Айналым',
  'Ставка': 'Мөлшерлеме',
  'текущий': 'ағымдағы',
  'История': 'Тарих',
  'Баланс после': 'Кейінгі баланс',
  'Пополнение': 'Толтыру',
  'Корректировка': 'Түзету',
  'Движений пока нет': 'Әзірге қозғалыс жоқ',
  'Пополнение баланса': 'Балансты толтыру',
  'Сумма пополнения (₸)': 'Толтыру сомасы (₸)',
  'Создать QR для оплаты': 'Төлеу үшін QR жасау',
  'Откройте оплату через Kaspi — баланс зачислится автоматически после оплаты.':
    'Kaspi арқылы төлемді ашыңыз — төлемнен кейін баланс автоматты түрде есептеледі.',
  'Открыть оплату Kaspi': 'Kaspi төлемін ашу',
  'Ожидаем оплату…': 'Төлемді күтудеміз…',
  'Баланс успешно пополнен': 'Баланс сәтті толтырылды',
  'Пополнение не завершено': 'Толтыру аяқталмады',
  'Пополнение временно недоступно': 'Толтыру уақытша қолжетімсіз',
  'Закрыть': 'Жабу',
  'Готово': 'Дайын',
  'от': 'бастап',
  // generic
  'Не авторизован': 'Авторизацияланбаған',
  'Ошибка запроса': 'Сұрау қатесі',
  'Загрузка…': 'Жүктелуде…',
  'Аккаунт': 'Аккаунт',
  'Выйти': 'Шығу',
  // statuses
  'успех': 'сәтті',
  'отказ': 'қабылданбады',
  'просрочен': 'мерзімі өткен',
  'ожидает': 'күтілуде',
  'все': 'барлығы',
  'просрочка': 'мерзімі өткен',
  'ожидают': 'күтілуде',
  // nav
  'Обзор': 'Шолу',
  'Операции': 'Операциялар',
  'Kaspi-касса': 'Kaspi кассасы',
  'API-ключи': 'API кілттері',
  'Настройки': 'Баптаулар',
  // sidebar
  'Кабинет': 'Кабинет',
  'Платежи': 'Төлемдер',
  'Разработчику': 'Әзірлеушіге',
  'Документация': 'Құжаттама',
  // topbar
  'Kaspi подключён': 'Kaspi қосылған',
  'Kaspi не подключён': 'Kaspi қосылмаған',
  '↻ Обновить': '↻ Жаңарту',
  // payment modal
  'Новая операция': 'Жаңа операция',
  '✕ закрыть': '✕ жабу',
  'QR-код': 'QR-код',
  'Счёт на телефон': 'Телефонға шот',
  'Возврат': 'Қайтарым',
  'Сумма (₸)': 'Сома (₸)',
  'Создание…': 'Құрылуда…',
  'Создать QR': 'QR құру',
  'Телефон клиента (7XXXXXXXXXX)': 'Клиент телефоны (7XXXXXXXXXX)',
  'Комментарий': 'Пікір',
  'необязательно': 'міндетті емес',
  'Отправка…': 'Жіберілуде…',
  'Выставить счёт': 'Шот қою',
  'QR Operation ID': 'QR Operation ID',
  'ID операции': 'Операция ID',
  'Сумма возврата (₸)': 'Қайтарым сомасы (₸)',
  'Возврат…': 'Қайтарылуда…',
  'Сделать возврат': 'Қайтарым жасау',
  'QR создан · #': 'QR құрылды · #',
  'Счёт отправлен клиенту · #': 'Шот клиентке жіберілді · #',
  'Возврат выполнен': 'Қайтарым орындалды',
  '→ открыть ссылку оплаты': '→ төлем сілтемесін ашу',
  // kpi
  'Объём · сегодня': 'Көлемі · бүгін',
  '─ успешные платежи': '─ сәтті төлемдер',
  'Платежей · сегодня': 'Төлемдер · бүгін',
  '─ успешно / всего': '─ сәтті / барлығы',
  '─ по завершённым': '─ аяқталғандар бойынша',
  'Всего операций': 'Барлық операциялар',
  '─ за всё время': '─ барлық уақытта',
  // overview
  'Кабинет': 'Кабинет',
  '+ Новая операция': '+ Жаңа операция',
  'Объём платежей': 'Төлемдер көлемі',
  '· сегодня · почасово': '· бүгін · сағат сайын',
  'успешно': 'сәтті',
  'отказ': 'қабылданбады',
  'просрочка': 'мерзімі өткен',
  'Всего операций · ': 'Барлық операциялар · ',
  'Последние операции': 'Соңғы операциялар',
  'Все →': 'Барлығы →',
  'Платежей пока нет': 'Әзірге төлемдер жоқ',
  'Время': 'Уақыт',
  'Сумма': 'Сома',
  'Тип': 'Түрі',
  'Статус': 'Күйі',
  'Счёт': 'Шот',
  'Kaspi-подключение': 'Kaspi қосылымы',
  '· сессия': '· сессия',
  'active': 'белсенді',
  'не подключено': 'қосылмаған',
  'Организация': 'Ұйым',
  'Kaspi не подключён': 'Kaspi қосылмаған',
  'подключите кассу, чтобы принимать платежи':
    'төлем қабылдау үшін кассаны қосыңыз',
  '+ Создать QR / счёт': '+ QR / шот құру',
  '⚙ Настроить Kaspi': '⚙ Kaspi баптау',
  '+ API-ключ': '+ API кілті',
  '⚙ Webhook-endpoint': '⚙ Webhook-endpoint',
  // ops page
  'операций за всё время · фильтр по статусу. Статусы подтягиваются из Kaspi автоматически.':
    'операция барлық уақытта · күй бойынша сүзгі. Күйлер Kaspi-ден автоматты түрде алынады.',
  'ID': 'ID',
  'Дата': 'Күні',
  'Kaspi-статус': 'Kaspi-күйі',
  'Операций не найдено': 'Операциялар табылмады',
  // kaspi page
  'Сессия Kaspi Pay — одна на аккаунт. Вход по номеру телефона роли кассира и SMS-коду; токен Kaspi хранится только на сервере.':
    'Kaspi Pay сессиясы — аккаунтқа біреу. Кіру кассир рөлінің телефон нөмірі мен SMS-код арқылы; Kaspi токені тек серверде сақталады.',
  'Статус сессии': 'Сессия күйі',
  'expired': 'мерзімі өткен',
  'торговая точка в Kaspi': 'Kaspi-дегі сауда нүктесі',
  'Телефон роли': 'Рөл телефоны',
  'кассир, через которого идёт приём': 'қабылдау жүретін кассир',
  'Управление сессией': 'Сессияны басқару',
  'обновить токен или отключить Kaspi': 'токенді жаңарту немесе Kaspi-ді ажырату',
  'Обновить': 'Жаңарту',
  'Отключить': 'Ажырату',
  'Kaspi Pay не подключён. Войдите по номеру телефона роли кассира, чтобы принимать платежи.':
    'Kaspi Pay қосылмаған. Төлем қабылдау үшін кассир рөлінің телефон нөмірімен кіріңіз.',
  'Инициализация…': 'Инициализация…',
  'Подключить Kaspi': 'Kaspi-ді қосу',
  'Номер телефона роли (7XXXXXXXXXX)': 'Рөл телефон нөмірі (7XXXXXXXXXX)',
  'Получить SMS-код': 'SMS-код алу',
  'SMS-код': 'SMS-код',
  'Проверка…': 'Тексерілуде…',
  'Подтвердить': 'Растау',
  'Отключить Kaspi Pay?': 'Kaspi Pay ажыратылсын ба?',
  'Не удалось отправить SMS': 'SMS жіберу мүмкін болмады',
  'SMS отправлено': 'SMS жіберілді',
  'Kaspi подключён': 'Kaspi қосылды',
  'Неверный код, попробуйте снова': 'Код қате, қайта көріңіз',
  'Сессия обновлена': 'Сессия жаңартылды',
  // keys page
  'Ключи для программного доступа к API. Передаются в заголовке X-Api-Key. Ключ показывается полностью один раз — сохраните его.':
    'API-ге бағдарламалық қол жеткізу кілттері. X-Api-Key тақырыбында жіберіледі. Кілт толық бір рет көрсетіледі — оны сақтаңыз.',
  'Создать ключ': 'Кілт құру',
  'Название ключа': 'Кілт атауы',
  'например, POS-касса 1': 'мысалы, POS-касса 1',
  'название': 'атауы',
  '+ Создать ключ': '+ Кілт құру',
  'Ключ создан и скопирован в буфер — позже он не отображается:':
    'Кілт құрылып, буферге көшірілді — кейін көрсетілмейді:',
  'Ваши ключи': 'Сіздің кілттеріңіз',
  'активных · ': 'белсенді · ',
  ' отозван': ' қайтарып алынды',
  'Ключей пока нет': 'Әзірге кілттер жоқ',
  'отозван': 'қайтарылды',
  'создан ': 'құрылды ',
  ' · использован ': ' · қолданылды ',
  'отозвать': 'қайтарып алу',
  'Отозвать ключ? Приложения с этим ключом потеряют доступ.':
    'Кілт қайтарып алынсын ба? Бұл кілттегі қосымшалар қол жеткізуін жоғалтады.',
  // webhooks page
  'Каждое событие подписывается HMAC-ключом в заголовке X-Webhook-Signature. Ретраи — до 3 раз с задержкой.':
    'Әр оқиға X-Webhook-Signature тақырыбында HMAC-кілтімен қол қойылады. Қайталаулар — кідіріспен 3 ретке дейін.',
  'Новый endpoint': 'Жаңа endpoint',
  'URL': 'URL',
  'куда слать POST с событием': 'оқиғамен POST қайда жіберілетіні',
  'Секрет': 'Құпия',
  'ключ для подписи X-Webhook-Signature': 'X-Webhook-Signature қолтаңбасының кілті',
  'платёж успешно завершён': 'төлем сәтті аяқталды',
  'платёж отклонён или отменён': 'төлем қабылданбады немесе бас тартылды',
  'QR просрочен без оплаты': 'QR төленбей мерзімі өтті',
  'Добавить webhook': 'Webhook қосу',
  'Добавить': 'Қосу',
  'Webhook добавлен': 'Webhook қосылды',
  'Активные endpoints': 'Белсенді endpoint-тер',
  'Webhook-ов пока нет': 'Әзірге webhook-тар жоқ',
  ' · подписан': ' · қол қойылған',
  ' · без подписи': ' · қолтаңбасыз',
  'вкл': 'қосулы',
  'выкл': 'өшірулі',
  'удалить': 'жою',
  'Удалить webhook?': 'Webhook жойылсын ба?',
  // settings page
  'Профиль SaaS-аккаунта. Вход — через Google, отдельного пароля нет.':
    'SaaS-аккаунт профилі. Кіру — Google арқылы, бөлек құпиясөз жоқ.',
  'Профиль': 'Профиль',
  'название аккаунта мерчанта': 'мерчант аккаунтының атауы',
  'Имя': 'Аты',
  'из аккаунта Google': 'Google аккаунтынан',
  'Email': 'Email',
  'Google · вход в кабинет': 'Google · кабинетке кіру',
  'Сессия': 'Сессия',
  'Выйти из кабинета': 'Кабинеттен шығу',
  'завершить текущую SSO-сессию': 'ағымдағы SSO-сессияны аяқтау',
};
const t = (s) => (KPOS_LANG === 'kk' ? (KK[s] || s) : s);

/* ─────────────────────────────────────────────────────────────
   API helpers — every call hits the real backend behind the
   dashboard session cookie.
   ───────────────────────────────────────────────────────────── */
async function api(url, opts) {
  const r = await fetch(url, opts);
  let body = {};
  try {
    body = await r.json();
  } catch {
    /* empty body */
  }
  if (r.status === 401) {
    location.href = '/login.html';
    throw new Error(t('Не авторизован'));
  }
  if (!r.ok) throw new Error(body.error || body.message || t('Ошибка запроса'));
  return body;
}
const post = (url, data) =>
  api(url, {
    method: 'POST',
    headers: data ? { 'Content-Type': 'application/json' } : undefined,
    body: data ? JSON.stringify(data) : undefined,
  });
const del = (url) => api(url, { method: 'DELETE' });

/* ─────────────────────────────────────────────────────────────
   Formatting + status helpers
   ───────────────────────────────────────────────────────────── */
function fmt(n) {
  if (!n && n !== 0) return '0';
  return Number(n).toLocaleString('ru-RU').replaceAll(',', ' ');
}
function timeOf(ts) {
  if (!ts) return '—';
  return new Date(ts).toLocaleTimeString('ru-RU', { hour12: false });
}
function dateOf(ts) {
  if (!ts) return '—';
  return new Date(ts).toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' });
}
function isToday(ts) {
  if (!ts) return false;
  const d = new Date(ts);
  const n = new Date();
  return (
    d.getDate() === n.getDate() &&
    d.getMonth() === n.getMonth() &&
    d.getFullYear() === n.getFullYear()
  );
}
// Map a backend payment row to one of the four display statuses.
function statusKind(p) {
  if (p.status === 'Processed') return 'ok';
  if (p.status === 'Expired' || p.status === 'QrTokenDiscarded') return 'exp';
  if (!p.is_final) return 'pen';
  return 'fail';
}
function amountOf(p) {
  return p.amount ?? p.meta?.amount ?? 0;
}

function StatPill({ status }) {
  const map = {
    ok: 'успех',
    fail: 'отказ',
    exp: 'просрочен',
    pen: 'ожидает',
  };
  return <span className={'stat-pill ' + status}>{t(map[status])}</span>;
}

/* ─────────────────────────────────────────────────────────────
   Volume chart — built from the tenant's real payments (today,
   hourly, stacked by outcome).
   ───────────────────────────────────────────────────────────── */
function VolumeChart({ payments }) {
  const hours = useMemo(() => {
    const out = Array.from({ length: 24 }, (_, h) => ({ h, success: 0, failed: 0, expired: 0 }));
    for (const p of payments) {
      if (!isToday(p.created_at)) continue;
      const h = new Date(p.created_at).getHours();
      const kind = statusKind(p);
      const amt = amountOf(p);
      if (kind === 'ok') out[h].success += amt;
      else if (kind === 'fail') out[h].failed += amt;
      else if (kind === 'exp') out[h].expired += amt;
    }
    return out;
  }, [payments]);

  const w = 100,
    h = 220;
  const max = Math.max(1, ...hours.map((d) => d.success + d.failed + d.expired));
  const barW = 100 / hours.length;
  const sy = (v) => h - 28 - (v / max) * (h - 48);

  return (
    <>
      <svg className="chart-svg" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
        {[0.25, 0.5, 0.75, 1].map((p) => (
          <line
            key={p}
            x1="0"
            x2={w}
            y1={h - 28 - p * (h - 48)}
            y2={h - 28 - p * (h - 48)}
            stroke="var(--rule-soft)"
            strokeWidth="0.2"
            strokeDasharray="0.5,0.5"
          />
        ))}
        {hours.map((d, i) => {
          const x = i * barW + barW * 0.12;
          const bw = barW * 0.76;
          const sSuc = sy(d.success);
          const sFail = sy(d.success + d.failed);
          const sExp = sy(d.success + d.failed + d.expired);
          const ground = h - 28;
          return (
            <g key={i}>
              <rect x={x} y={sSuc} width={bw} height={ground - sSuc} fill="var(--acid)" />
              <rect x={x} y={sFail} width={bw} height={sSuc - sFail} fill="var(--warn)" />
              <rect x={x} y={sExp} width={bw} height={sFail - sExp} fill="var(--mute)" opacity="0.5" />
            </g>
          );
        })}
      </svg>
      <div className="chart-hours">
        {['00:00', '06:00', '12:00', '18:00', '23:00'].map((hr) => (
          <span key={hr}>{hr}</span>
        ))}
      </div>
    </>
  );
}

/* ─────────────────────────────────────────────────────────────
   Sidebar
   ───────────────────────────────────────────────────────────── */
const NAV = [
  { id: 'overview', label: 'Обзор' },
  { id: 'ops', label: 'Операции' },
  { id: 'kaspi', label: 'Kaspi-касса' },
  { id: 'keys', label: 'API-ключи' },
  { id: 'billing', label: 'Биллинг' },
  { id: 'webhooks', label: 'Webhooks' },
  { id: 'settings', label: 'Настройки' },
];

/* Sidebar line icons — monochrome SVG, inherit color via currentColor. */
const ICONS = {
  overview: (
    <>
      <rect x="3" y="3" width="8" height="8" rx="1.5" />
      <rect x="13" y="3" width="8" height="8" rx="1.5" />
      <rect x="3" y="13" width="8" height="8" rx="1.5" />
      <rect x="13" y="13" width="8" height="8" rx="1.5" />
    </>
  ),
  ops: (
    <>
      <circle cx="4" cy="6" r="1.4" />
      <circle cx="4" cy="12" r="1.4" />
      <circle cx="4" cy="18" r="1.4" />
      <line x1="9" y1="6" x2="20" y2="6" />
      <line x1="9" y1="12" x2="20" y2="12" />
      <line x1="9" y1="18" x2="20" y2="18" />
    </>
  ),
  kaspi: (
    <>
      <rect x="2.5" y="5" width="19" height="14" rx="2.5" />
      <line x1="2.5" y1="9.5" x2="21.5" y2="9.5" />
      <line x1="6" y1="14.5" x2="11" y2="14.5" />
    </>
  ),
  keys: (
    <>
      <circle cx="8" cy="8" r="4.3" />
      <line x1="11" y1="11" x2="20" y2="20" />
      <line x1="18.6" y1="18.6" x2="20.6" y2="16.6" />
      <line x1="15.6" y1="15.6" x2="17.6" y2="13.6" />
    </>
  ),
  billing: (
    <>
      <rect x="2" y="6.5" width="20" height="11" rx="2" />
      <circle cx="12" cy="12" r="2.6" />
      <line x1="6" y1="10" x2="6" y2="14" />
      <line x1="18" y1="10" x2="18" y2="14" />
    </>
  ),
  webhooks: (
    <>
      <circle cx="5" cy="12" r="2.6" />
      <circle cx="19" cy="5.5" r="2.6" />
      <circle cx="19" cy="18.5" r="2.6" />
      <line x1="7.4" y1="10.9" x2="16.6" y2="6.6" />
      <line x1="7.4" y1="13.1" x2="16.6" y2="17.4" />
    </>
  ),
  settings: (
    <>
      <line x1="4" y1="8" x2="20" y2="8" />
      <line x1="4" y1="16" x2="20" y2="16" />
      <circle cx="14" cy="8" r="2.6" />
      <circle cx="9" cy="16" r="2.6" />
    </>
  ),
  docs: (
    <>
      <path d="M6 3.5h9l4 4v13H6z" />
      <polyline points="15 3.5 15 7.5 19 7.5" />
      <line x1="9.5" y1="12" x2="15.5" y2="12" />
      <line x1="9.5" y1="16" x2="15.5" y2="16" />
    </>
  ),
  admin: (
    <>
      <path d="M12 3l7.5 3v5.5c0 4.8-3.2 8.4-7.5 10.5-4.3-2.1-7.5-5.7-7.5-10.5V6z" />
    </>
  ),
};

function Icon({ name }) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      style={{ display: 'block', margin: '0 auto' }}
    >
      {ICONS[name]}
    </svg>
  );
}

function initialsOf(name, email) {
  const src = (name || email || '?').trim();
  const parts = src.split(/\s+/).filter(Boolean);
  if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
  return src.slice(0, 2).toUpperCase();
}

function Sidebar({ page, setPage, user, opsCount, onLogout }) {
  const link = (n) => (
    <a
      key={n.id}
      className={'side-link ' + (page === n.id ? 'active' : '')}
      onClick={() => setPage(n.id)}
    >
      <span className="sym"><Icon name={n.id} /></span>
      <span>{t(n.label)}</span>
      {n.id === 'ops' && opsCount != null && <span className="count">{opsCount}</span>}
    </a>
  );
  return (
    <aside className="side">
      <a href="/" className="brand">
        <span className="mark" />
        kaspipos<span style={{ color: 'var(--mute)' }}>/cabinet</span>
      </a>

      <div className="side-group">
        <div className="label">{t('Кабинет')}</div>
      </div>
      {NAV.slice(0, 1).map(link)}

      <div className="side-divider" />
      <div className="side-group">
        <div className="label">{t('Платежи')}</div>
      </div>
      {NAV.slice(1, 4).map(link)}

      <div className="side-divider" />
      <div className="side-group">
        <div className="label">{t('Финансы')}</div>
      </div>
      {NAV.slice(4, 5).map(link)}

      <div className="side-divider" />
      <div className="side-group">
        <div className="label">{t('Разработчику')}</div>
      </div>
      {NAV.slice(5, 7).map(link)}
      <a className="side-link" href="/docs.html" target="_blank" rel="noopener">
        <span className="sym"><Icon name="docs" /></span>
        <span>{t('Документация')}</span>
        <span className="count">v3</span>
      </a>
      {user?.isAdmin && (
        <a className="side-link" href="/admin.html">
          <span className="sym"><Icon name="admin" /></span>
          <span>{t('Админка')}</span>
        </a>
      )}

      <div className="side-user">
        <div className="av">{initialsOf(user?.name, user?.email)}</div>
        <div className="who">
          <b>{user?.name || t('Аккаунт')}</b>
          <span>{user?.email || '—'}</span>
        </div>
        <a className="logout" onClick={onLogout} title={t('Выйти')}>
          ↗
        </a>
      </div>
    </aside>
  );
}

/* ─────────────────────────────────────────────────────────────
   Language switch
   ───────────────────────────────────────────────────────────── */
function LangSwitch() {
  const L = window.KPOS_LANG || 'ru';
  const mk = (code, label) => (
    <button key={code} onClick={() => window.kposSetLang(code)}
      style={{ font: "500 11px var(--mono, 'JetBrains Mono', monospace)", letterSpacing: '0.04em',
        padding: '4px 8px', borderRadius: 3, cursor: 'pointer', border: 0,
        background: L === code ? 'var(--ink)' : 'transparent',
        color: L === code ? 'var(--paper)' : 'var(--mute)' }}>{label}</button>
  );
  return (<div style={{ display: 'inline-flex', gap: 2, border: '1px solid var(--rule-soft)', borderRadius: 4, padding: 2 }}>{mk('ru','РУ')}{mk('kk','ҚАЗ')}</div>);
}

/* ─────────────────────────────────────────────────────────────
   Topbar
   ───────────────────────────────────────────────────────────── */
function Topbar({ page, kaspi, onReload }) {
  const crumbs = {
    overview: 'Обзор',
    ops: 'Операции',
    kaspi: 'Kaspi-касса',
    keys: 'API-ключи',
    billing: 'Биллинг',
    webhooks: 'Webhooks',
    settings: 'Настройки',
  };
  return (
    <div className="topbar">
      <div className="crumbs">
        Cabinet · <b>{t(crumbs[page])}</b>
      </div>
      <div className="grow" />
      <div
        className="env-switch"
        title={kaspi?.connected ? t('Kaspi подключён') : t('Kaspi не подключён')}
      >
        <button className={kaspi?.connected ? 'active live' : ''}>
          {kaspi?.connected ? '● Kaspi' : '○ Kaspi'}
        </button>
      </div>
      <LangSwitch />
      <button className="btn btn-ghost btn-sm" onClick={onReload}>
        {t('↻ Обновить')}
      </button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Payment modal — create QR / invoice, or make a refund.
   ───────────────────────────────────────────────────────────── */
function PaymentModal({ onClose, onDone }) {
  const [tab, setTab] = useState('qr');
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null); // { kind: 'ok'|'err', text, link }

  // form fields
  const [qrAmount, setQrAmount] = useState(1000);
  const [invPhone, setInvPhone] = useState('');
  const [invAmount, setInvAmount] = useState(1000);
  const [invComment, setInvComment] = useState('');
  const [refId, setRefId] = useState('');
  const [refAmount, setRefAmount] = useState(0);

  async function createQr() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/qr/create', { amount: Number(qrAmount) });
      const d = r.Data || {};
      if (d.QrToken) {
        setMsg({ kind: 'ok', text: t('QR создан · #') + d.QrOperationId, link: d.QrToken });
        onDone();
      } else {
        setMsg({ kind: 'err', text: 'Kaspi: ' + (r.Message || JSON.stringify(r)) });
      }
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }

  async function createInvoice() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/invoice/create', {
        phoneNumber: invPhone.trim(),
        amount: Number(invAmount),
        comment: invComment.trim(),
      });
      const d = r.Data || {};
      if (d.Id) {
        setMsg({ kind: 'ok', text: t('Счёт отправлен клиенту · #') + d.Id });
        onDone();
      } else {
        setMsg({ kind: 'err', text: 'Kaspi: ' + (r.Message || JSON.stringify(r)) });
      }
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }

  async function makeRefund() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/refund/create', {
        qrOperationId: refId.trim(),
        returnAmount: Number(refAmount),
      });
      const ok = !r.StatusCode || r.StatusCode === 0;
      setMsg({
        kind: ok ? 'ok' : 'err',
        text: ok ? t('Возврат выполнен') : 'Kaspi: ' + (r.Message || JSON.stringify(r)),
      });
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }

  const fieldStyle = {
    width: '100%',
    fontFamily: 'JetBrains Mono',
    fontSize: 13,
    padding: '11px 13px',
    border: '1px solid var(--rule-soft)',
    background: 'var(--paper)',
    borderRadius: 3,
    outline: 'none',
    marginTop: 6,
  };
  const labelStyle = {
    fontFamily: 'JetBrains Mono',
    fontSize: 11,
    letterSpacing: '0.04em',
    textTransform: 'uppercase',
    color: 'var(--mute)',
    marginTop: 16,
    display: 'block',
  };

  return (
    <div
      onClick={onClose}
      style={{
        position: 'fixed',
        inset: 0,
        background: 'rgba(15, 15, 16, 0.55)',
        display: 'grid',
        placeItems: 'center',
        zIndex: 200,
        padding: 24,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: '100%',
          maxWidth: 460,
          background: 'var(--paper)',
          border: '1px solid var(--ink)',
          borderRadius: 4,
        }}
      >
        <div className="panel-head" style={{ borderBottom: '1px solid var(--ink)' }}>
          <h2>{t('Новая операция')}</h2>
          <div className="right">
            <a onClick={onClose} style={{ cursor: 'pointer' }}>
              {t('✕ закрыть')}
            </a>
          </div>
        </div>

        <div style={{ display: 'flex', borderBottom: '1px solid var(--rule-soft)' }}>
          {[
            ['qr', t('QR-код')],
            ['invoice', t('Счёт на телефон')],
            ['refund', t('Возврат')],
          ].map(([k, l]) => (
            <a
              key={k}
              onClick={() => {
                setTab(k);
                setMsg(null);
              }}
              style={{
                flex: 1,
                textAlign: 'center',
                padding: '12px 8px',
                cursor: 'pointer',
                fontSize: 13,
                fontWeight: 500,
                color: tab === k ? 'var(--ink)' : 'var(--mute)',
                borderBottom: tab === k ? '2px solid var(--ink)' : '2px solid transparent',
              }}
            >
              {l}
            </a>
          ))}
        </div>

        <div style={{ padding: '8px 22px 22px' }}>
          {tab === 'qr' && (
            <>
              <label style={labelStyle}>{t('Сумма (₸)')}</label>
              <input
                type="number"
                min="1"
                value={qrAmount}
                onChange={(e) => setQrAmount(e.target.value)}
                style={fieldStyle}
              />
              <button
                className="btn btn-ink"
                style={{ marginTop: 18, width: '100%' }}
                disabled={busy}
                onClick={createQr}
              >
                {busy ? t('Создание…') : t('Создать QR')}
              </button>
            </>
          )}

          {tab === 'invoice' && (
            <>
              <label style={labelStyle}>{t('Телефон клиента (7XXXXXXXXXX)')}</label>
              <input
                type="tel"
                placeholder="77001234567"
                value={invPhone}
                onChange={(e) => setInvPhone(e.target.value)}
                style={fieldStyle}
              />
              <label style={labelStyle}>{t('Сумма (₸)')}</label>
              <input
                type="number"
                min="1"
                value={invAmount}
                onChange={(e) => setInvAmount(e.target.value)}
                style={fieldStyle}
              />
              <label style={labelStyle}>{t('Комментарий')}</label>
              <input
                type="text"
                placeholder={t('необязательно')}
                value={invComment}
                onChange={(e) => setInvComment(e.target.value)}
                style={fieldStyle}
              />
              <button
                className="btn btn-ink"
                style={{ marginTop: 18, width: '100%' }}
                disabled={busy}
                onClick={createInvoice}
              >
                {busy ? t('Отправка…') : t('Выставить счёт')}
              </button>
            </>
          )}

          {tab === 'refund' && (
            <>
              <label style={labelStyle}>{t('QR Operation ID')}</label>
              <input
                type="text"
                placeholder={t('ID операции')}
                value={refId}
                onChange={(e) => setRefId(e.target.value)}
                style={fieldStyle}
              />
              <label style={labelStyle}>{t('Сумма возврата (₸)')}</label>
              <input
                type="number"
                min="1"
                value={refAmount}
                onChange={(e) => setRefAmount(e.target.value)}
                style={fieldStyle}
              />
              <button
                className="btn btn-ink"
                style={{ marginTop: 18, width: '100%' }}
                disabled={busy}
                onClick={makeRefund}
              >
                {busy ? t('Возврат…') : t('Сделать возврат')}
              </button>
            </>
          )}

          {msg && (
            <div
              style={{
                marginTop: 16,
                fontFamily: 'JetBrains Mono',
                fontSize: 12,
                lineHeight: 1.6,
                color: msg.kind === 'ok' ? 'var(--ok)' : 'var(--warn)',
                wordBreak: 'break-all',
              }}
            >
              {msg.text}
              {msg.link && (
                <>
                  <br />
                  <a href={msg.link} target="_blank" rel="noopener">
                    {t('→ открыть ссылку оплаты')}
                  </a>
                </>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   KPI strip — computed from real payments
   ───────────────────────────────────────────────────────────── */
function KpiStrip({ payments }) {
  const k = useMemo(() => {
    const today = payments.filter((p) => isToday(p.created_at));
    const todayOk = today.filter((p) => statusKind(p) === 'ok');
    const todayVolume = todayOk.reduce((s, p) => s + amountOf(p), 0);
    const final = payments.filter((p) => p.is_final);
    const okAll = final.filter((p) => statusKind(p) === 'ok');
    const rate = final.length ? Math.round((okAll.length / final.length) * 1000) / 10 : 0;
    return {
      todayVolume,
      todayCount: today.length,
      todayOk: todayOk.length,
      rate,
      total: payments.length,
    };
  }, [payments]);

  return (
    <div className="kpi-strip">
      <div className="kpi">
        <div className="k">
          <span>{t('Объём · сегодня')}</span>
          <span className="num">01</span>
        </div>
        <div className="v">
          {fmt(k.todayVolume)}
          <span className="cur">₸</span>
        </div>
        <div className="delta flat">{t('─ успешные платежи')}</div>
      </div>
      <div className="kpi">
        <div className="k">
          <span>{t('Платежей · сегодня')}</span>
          <span className="num">02</span>
        </div>
        <div className="v">
          {k.todayOk}
          <span className="it">/{k.todayCount}</span>
        </div>
        <div className="delta flat">{t('─ успешно / всего')}</div>
      </div>
      <div className="kpi">
        <div className="k">
          <span>Success rate</span>
          <span className="num">03</span>
        </div>
        <div className="v">
          {k.rate}
          <span className="it">%</span>
        </div>
        <div className="delta flat">{t('─ по завершённым')}</div>
      </div>
      <div className="kpi">
        <div className="k">
          <span>{t('Всего операций')}</span>
          <span className="num">04</span>
        </div>
        <div className="v">{fmt(k.total)}</div>
        <div className="delta flat">{t('─ за всё время')}</div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Overview page
   ───────────────────────────────────────────────────────────── */
function Overview({ user, payments, kaspi, setPage, openModal }) {
  const firstName = (user?.name || '').trim().split(/\s+/)[0] || '';
  const recent = payments.slice(0, 8);
  const todayVolume = payments
    .filter((p) => isToday(p.created_at) && statusKind(p) === 'ok')
    .reduce((s, p) => s + amountOf(p), 0);

  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            {t('Кабинет')}{firstName ? ', ' : ''}
            {firstName && <span className="it">{firstName}.</span>}
          </h1>
          <p>
            {KPOS_LANG === 'kk'
              ? `Бүгін — ${fmt(todayVolume)} ₸ сәтті төлемдер. Kaspi қосылымы: ${
                  kaspi?.connected ? 'белсенді.' : 'бапталмаған.'
                }`
              : `За сегодня — ${fmt(todayVolume)} ₸ успешных платежей. Подключение Kaspi: ${
                  kaspi?.connected ? 'активно.' : 'не настроено.'
                }`}
          </p>
        </div>
        <div className="actions">
          <button className="btn btn-ink btn-sm" onClick={openModal}>
            {t('+ Новая операция')}
          </button>
        </div>
      </div>

      <KpiStrip payments={payments} />

      <div className="content">
        <div className="panel chart-panel">
          <div className="panel-head">
            <h2>{t('Объём платежей')}</h2>
            <span className="sub">{t('· сегодня · почасово')}</span>
          </div>
          <div className="chart-wrap">
            <VolumeChart payments={payments} />
          </div>
          <div className="chart-legend">
            <span>
              <span className="dot s" />
              {t('успешно')}
            </span>
            <span>
              <span className="dot f" />
              {t('отказ')}
            </span>
            <span>
              <span className="dot e" />
              {t('просрочка')}
            </span>
            <span className="grow" />
            <span>{t('Всего операций · ')}{payments.length}</span>
          </div>
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Последние операции')}</h2>
            <span className="sub">· {recent.length}</span>
            <div className="right">
              <a onClick={() => setPage('ops')}>{t('Все →')}</a>
            </div>
          </div>
          {recent.length === 0 ? (
            <div className="empty">
              <div className="glyph">∅</div>
              {t('Платежей пока нет')}
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>{t('Время')}</th>
                  <th>{t('Сумма')}</th>
                  <th>{t('Тип')}</th>
                  <th>{t('Статус')}</th>
                </tr>
              </thead>
              <tbody>
                {recent.map((p) => (
                  <tr key={p.payment_id}>
                    <td className="col-t">{timeOf(p.created_at)}</td>
                    <td className="col-amt">{fmt(amountOf(p))} ₸</td>
                    <td className="col-ref">{p.type === 'qr' ? 'QR' : t('Счёт')}</td>
                    <td>
                      <StatPill status={statusKind(p)} />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Kaspi-подключение')}</h2>
            <span className="sub">{t('· сессия')}</span>
            <div
              className="right"
              style={{ color: kaspi?.connected ? 'var(--ok)' : 'var(--warn)' }}
            >
              ● {kaspi?.connected ? t('active') : t('не подключено')}
            </div>
          </div>
          <div className="balance">
            <div className="k">{t('Организация')}</div>
            <div className="v" style={{ fontSize: 28 }}>
              {kaspi?.connected ? kaspi.orgName || '—' : t('Kaspi не подключён')}
            </div>
            <div className="sub">
              {kaspi?.connected
                ? `${KPOS_LANG === 'kk' ? 'телефон' : 'телефон'} ${kaspi.phone || '—'}`
                : t('подключите кассу, чтобы принимать платежи')}
            </div>
          </div>
          <div className="quick">
            <a onClick={openModal}>
              <span className="lbl">a / payment</span>
              <span className="v">{t('+ Создать QR / счёт')}</span>
            </a>
            <a onClick={() => setPage('kaspi')}>
              <span className="lbl">b / kaspi</span>
              <span className="v">{t('⚙ Настроить Kaspi')}</span>
            </a>
            <a onClick={() => setPage('keys')}>
              <span className="lbl">c / dev</span>
              <span className="v">{t('+ API-ключ')}</span>
            </a>
            <a onClick={() => setPage('webhooks')}>
              <span className="lbl">d / webhook</span>
              <span className="v">{t('⚙ Webhook-endpoint')}</span>
            </a>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Operations page
   ───────────────────────────────────────────────────────────── */
function OpsPage({ payments, openModal }) {
  const [filter, setFilter] = useState('all');
  const filtered = useMemo(
    () => payments.filter((p) => (filter === 'all' ? true : statusKind(p) === filter)),
    [payments, filter]
  );
  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            {KPOS_LANG === 'kk' ? 'Барлық ' : 'Все '}
            <span className="it">{KPOS_LANG === 'kk' ? 'операциялар.' : 'операции.'}</span>
          </h1>
          <p>
            {payments.length}{' '}
            {t(
              'операций за всё время · фильтр по статусу. Статусы подтягиваются из Kaspi автоматически.'
            )}
          </p>
        </div>
        <div className="actions">
          <button className="btn btn-ink btn-sm" onClick={openModal}>
            {t('+ Новая операция')}
          </button>
        </div>
      </div>
      <div className="content" style={{ gridTemplateColumns: '1fr' }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Операции')}</h2>
            <span className="sub">· {filtered.length}</span>
            <div className="right" style={{ gap: 8 }}>
              {[
                ['all', t('все')],
                ['ok', t('успех')],
                ['fail', t('отказ')],
                ['exp', t('просрочка')],
                ['pen', t('ожидают')],
              ].map(([k, l]) => (
                <a
                  key={k}
                  onClick={() => setFilter(k)}
                  style={{
                    cursor: 'pointer',
                    color: filter === k ? 'var(--ink)' : 'var(--mute)',
                    fontWeight: filter === k ? 600 : 500,
                    borderBottom: filter === k ? '1px solid var(--ink)' : '0',
                    paddingBottom: 1,
                  }}
                >
                  {l}
                </a>
              ))}
            </div>
          </div>
          {filtered.length === 0 ? (
            <div className="empty">
              <div className="glyph">∅</div>
              {t('Операций не найдено')}
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>{t('ID')}</th>
                  <th>{t('Дата')}</th>
                  <th>{t('Время')}</th>
                  <th>{t('Сумма')}</th>
                  <th>{t('Тип')}</th>
                  <th>{t('Kaspi-статус')}</th>
                  <th>{t('Статус')}</th>
                </tr>
              </thead>
              <tbody>
                {filtered.map((p) => (
                  <tr key={p.payment_id}>
                    <td className="col-ref">{p.payment_id}</td>
                    <td className="col-t">{dateOf(p.created_at)}</td>
                    <td className="col-t">{timeOf(p.created_at)}</td>
                    <td className="col-amt">{fmt(amountOf(p))} ₸</td>
                    <td className="col-ref">{p.type === 'qr' ? 'QR' : t('Счёт')}</td>
                    <td className="col-ref">{p.status}</td>
                    <td>
                      <StatPill status={statusKind(p)} />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Kaspi page — connect / refresh / disconnect the Kaspi session
   ───────────────────────────────────────────────────────────── */
function KaspiPage({ kaspi, reloadKaspi }) {
  // phase: idle | phone | otp
  const [phase, setPhase] = useState('idle');
  const [phone, setPhone] = useState('');
  const [otp, setOtp] = useState('');
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null); // { kind, text }

  async function start() {
    setBusy(true);
    setMsg(null);
    try {
      await post('/api/auth/init');
      setPhase('phone');
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }
  async function sendPhone() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/auth/send-phone', { phoneNumber: phone.trim() });
      if (!r.success) throw new Error(r.desc || t('Не удалось отправить SMS'));
      setMsg({ kind: 'ok', text: t('SMS отправлено') });
      setPhase('otp');
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }
  async function verifyOtp() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/auth/verify-otp', { otp: otp.trim() });
      if (!r.success) throw new Error(t('Неверный код, попробуйте снова'));
      setMsg({ kind: 'ok', text: t('Kaspi подключён') });
      setPhase('idle');
      setPhone('');
      setOtp('');
      reloadKaspi();
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }
  async function refresh() {
    setBusy(true);
    setMsg(null);
    try {
      const r = await post('/api/auth/refresh');
      setMsg({ kind: r.success ? 'ok' : 'err', text: r.success ? t('Сессия обновлена') : r.message });
      reloadKaspi();
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }
  async function disconnect() {
    if (!window.confirm(t('Отключить Kaspi Pay?'))) return;
    setBusy(true);
    setMsg(null);
    try {
      await post('/api/auth/disconnect');
      setPhase('idle');
      reloadKaspi();
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }

  const inputStyle = {
    width: '100%',
    fontFamily: 'JetBrains Mono',
    fontSize: 14,
    padding: '12px 14px',
    border: '1px solid var(--ink)',
    background: 'var(--paper)',
    borderRadius: 3,
    outline: 'none',
  };

  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            {KPOS_LANG === 'kk' ? 'Kaspi ' : 'Подключение '}
            <span className="it">{KPOS_LANG === 'kk' ? 'қосылымы.' : 'Kaspi.'}</span>
          </h1>
          <p>
            {t(
              'Сессия Kaspi Pay — одна на аккаунт. Вход по номеру телефона роли кассира и SMS-коду; токен Kaspi хранится только на сервере.'
            )}
          </p>
        </div>
      </div>

      <div className="content" style={{ gridTemplateColumns: '1fr' }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Статус сессии')}</h2>
            <div
              className="right"
              style={{ color: kaspi?.connected ? 'var(--ok)' : 'var(--warn)' }}
            >
              ●{' '}
              {kaspi?.connected
                ? t('active')
                : kaspi?.status === 'expired'
                  ? t('expired')
                  : t('не подключено')}
            </div>
          </div>

          {kaspi?.connected ? (
            <>
              <div className="set-row">
                <div className="lbl">
                  {t('Организация')}
                  <span>{t('торговая точка в Kaspi')}</span>
                </div>
                <div style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>
                  {kaspi.orgName || '—'}
                </div>
                <span />
              </div>
              <div className="set-row">
                <div className="lbl">
                  {t('Телефон роли')}
                  <span>{t('кассир, через которого идёт приём')}</span>
                </div>
                <div style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>
                  {kaspi.phone || '—'}
                </div>
                <span />
              </div>
              <div className="set-row">
                <div className="lbl">
                  {t('Управление сессией')}
                  <span>{t('обновить токен или отключить Kaspi')}</span>
                </div>
                <span />
                <div style={{ display: 'flex', gap: 8 }}>
                  <button className="btn btn-ghost btn-sm" disabled={busy} onClick={refresh}>
                    {t('Обновить')}
                  </button>
                  <button
                    className="btn btn-ghost btn-sm"
                    disabled={busy}
                    onClick={disconnect}
                    style={{ borderColor: 'var(--warn)', color: 'var(--warn)' }}
                  >
                    {t('Отключить')}
                  </button>
                </div>
              </div>
            </>
          ) : (
            <div style={{ padding: 24 }}>
              {phase === 'idle' && (
                <>
                  <p style={{ fontSize: 14, color: 'var(--mute)', marginBottom: 16 }}>
                    {t(
                      'Kaspi Pay не подключён. Войдите по номеру телефона роли кассира, чтобы принимать платежи.'
                    )}
                  </p>
                  <button className="btn btn-ink" disabled={busy} onClick={start}>
                    {busy ? t('Инициализация…') : t('Подключить Kaspi')}
                  </button>
                </>
              )}
              {phase === 'phone' && (
                <>
                  <label
                    style={{
                      fontFamily: 'JetBrains Mono',
                      fontSize: 11,
                      letterSpacing: '0.04em',
                      textTransform: 'uppercase',
                      color: 'var(--mute)',
                    }}
                  >
                    {t('Номер телефона роли (7XXXXXXXXXX)')}
                  </label>
                  <input
                    type="tel"
                    placeholder="77001234567"
                    value={phone}
                    onChange={(e) => setPhone(e.target.value)}
                    style={{ ...inputStyle, margin: '8px 0 16px' }}
                  />
                  <button className="btn btn-ink" disabled={busy} onClick={sendPhone}>
                    {busy ? t('Отправка…') : t('Получить SMS-код')}
                  </button>
                </>
              )}
              {phase === 'otp' && (
                <>
                  <label
                    style={{
                      fontFamily: 'JetBrains Mono',
                      fontSize: 11,
                      letterSpacing: '0.04em',
                      textTransform: 'uppercase',
                      color: 'var(--mute)',
                    }}
                  >
                    {t('SMS-код')}
                  </label>
                  <input
                    type="text"
                    inputMode="numeric"
                    placeholder="123456"
                    value={otp}
                    onChange={(e) => setOtp(e.target.value)}
                    style={{ ...inputStyle, margin: '8px 0 16px' }}
                  />
                  <button className="btn btn-ink" disabled={busy} onClick={verifyOtp}>
                    {busy ? t('Проверка…') : t('Подтвердить')}
                  </button>
                </>
              )}
              {msg && (
                <div
                  style={{
                    marginTop: 16,
                    fontFamily: 'JetBrains Mono',
                    fontSize: 12,
                    color: msg.kind === 'ok' ? 'var(--ok)' : 'var(--warn)',
                  }}
                >
                  {msg.text}
                </div>
              )}
            </div>
          )}

          {kaspi?.connected && msg && (
            <div
              className="set-row"
              style={{
                fontFamily: 'JetBrains Mono',
                fontSize: 12,
                color: msg.kind === 'ok' ? 'var(--ok)' : 'var(--warn)',
              }}
            >
              {msg.text}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   API keys page
   ───────────────────────────────────────────────────────────── */
function KeysPage() {
  const [keys, setKeys] = useState([]);
  const [name, setName] = useState('');
  const [busy, setBusy] = useState(false);
  const [newKey, setNewKey] = useState(null);
  const [err, setErr] = useState('');

  const load = useCallback(async () => {
    try {
      const { keys } = await api('/api/keys');
      setKeys(keys);
    } catch (e) {
      setErr(e.message);
    }
  }, []);
  useEffect(() => {
    load();
  }, [load]);

  async function create() {
    setBusy(true);
    setErr('');
    try {
      const k = await post('/api/keys', { name: name.trim() });
      setNewKey(k.key);
      setName('');
      try {
        await navigator.clipboard.writeText(k.key);
      } catch {
        /* clipboard optional */
      }
      load();
      setTimeout(() => setNewKey(null), 12000);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }
  async function revoke(id) {
    if (!window.confirm(t('Отозвать ключ? Приложения с этим ключом потеряют доступ.'))) return;
    try {
      await del('/api/keys/' + id);
      load();
    } catch (e) {
      setErr(e.message);
    }
  }

  const active = keys.filter((k) => !k.revoked_at);
  const revoked = keys.filter((k) => k.revoked_at);

  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            API-<span className="it">{KPOS_LANG === 'kk' ? 'кілттер.' : 'ключи.'}</span>
          </h1>
          <p>
            {t(
              'Ключи для программного доступа к API. Передаются в заголовке X-Api-Key. Ключ показывается полностью один раз — сохраните его.'
            )}
          </p>
        </div>
      </div>

      <div className="content" style={{ gridTemplateColumns: '1fr' }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Создать ключ')}</h2>
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Название ключа')}
              <span>{t('например, POS-касса 1')}</span>
            </div>
            <input
              type="text"
              value={name}
              placeholder={t('название')}
              onChange={(e) => setName(e.target.value)}
            />
            <button className="btn btn-ink btn-sm" disabled={busy} onClick={create}>
              {busy ? '…' : t('+ Создать ключ')}
            </button>
          </div>
          {newKey && (
            <div
              className="set-row"
              style={{ gridTemplateColumns: '1fr', background: 'var(--paper-2)' }}
            >
              <div style={{ fontFamily: 'JetBrains Mono', fontSize: 12, lineHeight: 1.7 }}>
                {t('Ключ создан и скопирован в буфер — позже он не отображается:')}
                <br />
                <b style={{ color: 'var(--ok)', wordBreak: 'break-all' }}>{newKey}</b>
              </div>
            </div>
          )}
          {err && (
            <div
              className="set-row"
              style={{ fontFamily: 'JetBrains Mono', fontSize: 12, color: 'var(--warn)' }}
            >
              {err}
            </div>
          )}
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Ваши ключи')}</h2>
            <span className="sub">
              · {active.length} {t('активных · ')}{revoked.length}{t(' отозван')}
            </span>
          </div>
          {keys.length === 0 ? (
            <div className="empty">
              <div className="glyph">∅</div>
              {t('Ключей пока нет')}
            </div>
          ) : (
            <div className="list">
              {keys.map((k) => (
                <div
                  className="list-row key-row"
                  key={k.id}
                  style={{ opacity: k.revoked_at ? 0.45 : 1 }}
                >
                  <span className="sym">{k.revoked_at ? '○' : '●'}</span>
                  <div>
                    <div className="name">
                      {k.name}
                      {k.revoked_at && (
                        <span
                          style={{
                            marginLeft: 8,
                            fontFamily: 'JetBrains Mono',
                            fontSize: 11,
                            color: 'var(--warn)',
                            textTransform: 'uppercase',
                            letterSpacing: '0.04em',
                          }}
                        >
                          {t('отозван')}
                        </span>
                      )}
                    </div>
                    <div className="meta">
                      <b>{k.prefix}…</b> · {t('создан ')}{dateOf(k.created_at)}
                      {t(' · использован ')}
                      {k.last_used_at ? dateOf(k.last_used_at) : '—'}
                    </div>
                  </div>
                  <span className="pill live">key</span>
                  <span />
                  <span
                    className="copy"
                    style={{
                      color: k.revoked_at ? 'var(--mute)' : 'var(--warn)',
                      borderColor: k.revoked_at ? 'var(--rule-soft)' : 'rgba(255, 91, 31, 0.4)',
                      cursor: k.revoked_at ? 'default' : 'pointer',
                    }}
                    onClick={() => !k.revoked_at && revoke(k.id)}
                  >
                    {k.revoked_at ? '—' : t('отозвать')}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Webhooks page
   ───────────────────────────────────────────────────────────── */
const WH_EVENTS = ['payment.success', 'payment.failed', 'payment.expired'];

function WebhooksPage() {
  const [hooks, setHooks] = useState([]);
  const [url, setUrl] = useState('');
  const [secret, setSecret] = useState('');
  const [events, setEvents] = useState({
    'payment.success': true,
    'payment.failed': true,
    'payment.expired': true,
  });
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);

  const load = useCallback(async () => {
    try {
      const { webhooks } = await api('/api/webhooks');
      setHooks(webhooks);
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    }
  }, []);
  useEffect(() => {
    load();
  }, [load]);

  async function create() {
    setBusy(true);
    setMsg(null);
    try {
      const evList = WH_EVENTS.filter((e) => events[e]);
      await post('/api/webhooks', { url: url.trim(), events: evList, secret: secret.trim() });
      setUrl('');
      setSecret('');
      setMsg({ kind: 'ok', text: t('Webhook добавлен') });
      load();
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    } finally {
      setBusy(false);
    }
  }
  async function remove(id) {
    if (!window.confirm(t('Удалить webhook?'))) return;
    try {
      await del('/api/webhooks/' + id);
      load();
    } catch (e) {
      setMsg({ kind: 'err', text: e.message });
    }
  }

  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            <span className="it">Webhooks</span> · endpoints.
          </h1>
          <p>
            {t(
              'Каждое событие подписывается HMAC-ключом в заголовке X-Webhook-Signature. Ретраи — до 3 раз с задержкой.'
            )}
          </p>
        </div>
      </div>

      <div className="content" style={{ gridTemplateColumns: '1fr' }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Новый endpoint')}</h2>
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('URL')}
              <span>{t('куда слать POST с событием')}</span>
            </div>
            <input
              type="url"
              placeholder="https://example.com/webhook"
              value={url}
              onChange={(e) => setUrl(e.target.value)}
            />
            <span />
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Секрет')}
              <span>{t('ключ для подписи X-Webhook-Signature')}</span>
            </div>
            <input
              type="text"
              placeholder={t('необязательно')}
              value={secret}
              onChange={(e) => setSecret(e.target.value)}
            />
            <span />
          </div>
          {WH_EVENTS.map((e) => (
            <div className="set-row" key={e}>
              <div className="lbl">
                {e}
                <span>
                  {e === 'payment.success'
                    ? t('платёж успешно завершён')
                    : e === 'payment.failed'
                      ? t('платёж отклонён или отменён')
                      : t('QR просрочен без оплаты')}
                </span>
              </div>
              <span />
              <div
                className={'toggle ' + (events[e] ? 'on' : '')}
                onClick={() => setEvents({ ...events, [e]: !events[e] })}
              />
            </div>
          ))}
          <div className="set-row">
            <div className="lbl">{t('Добавить webhook')}</div>
            <span />
            <button className="btn btn-ink btn-sm" disabled={busy} onClick={create}>
              {busy ? '…' : t('Добавить')}
            </button>
          </div>
          {msg && (
            <div
              className="set-row"
              style={{
                fontFamily: 'JetBrains Mono',
                fontSize: 12,
                color: msg.kind === 'ok' ? 'var(--ok)' : 'var(--warn)',
              }}
            >
              {msg.text}
            </div>
          )}
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Активные endpoints')}</h2>
            <span className="sub">· {hooks.length}</span>
          </div>
          {hooks.length === 0 ? (
            <div className="empty">
              <div className="glyph">∅</div>
              {t('Webhook-ов пока нет')}
            </div>
          ) : (
            <div className="list">
              {hooks.map((w) => (
                <div className="list-row device-row" key={w.id}>
                  <div className="ic">↳</div>
                  <div>
                    <div className="name" style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>
                      {w.url}
                    </div>
                    <div className="meta">
                      {(w.events || []).join(' · ')}
                      {w.secret ? t(' · подписан') : t(' · без подписи')}
                    </div>
                  </div>
                  <span className={'pill ' + (w.enabled ? 'on' : 'off')}>
                    {w.enabled ? t('вкл') : t('выкл')}
                  </span>
                  <span />
                  <span
                    className="copy"
                    style={{ color: 'var(--warn)', borderColor: 'rgba(255, 91, 31, 0.4)' }}
                    onClick={() => remove(w.id)}
                  >
                    {t('удалить')}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Settings page
   ───────────────────────────────────────────────────────────── */
function SettingsPage({ user, onLogout }) {
  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            <span className="it">{t('Настройки')}</span>{' '}
            {KPOS_LANG === 'kk' ? 'аккаунты.' : 'аккаунта.'}
          </h1>
          <p>{t('Профиль SaaS-аккаунта. Вход — через Google, отдельного пароля нет.')}</p>
        </div>
      </div>

      <div className="content" style={{ gridTemplateColumns: '1fr' }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Профиль')}</h2>
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Организация')}
              <span>{t('название аккаунта мерчанта')}</span>
            </div>
            <div style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>
              {user?.tenantName || '—'}
            </div>
            <span />
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Имя')}
              <span>{t('из аккаунта Google')}</span>
            </div>
            <div style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>{user?.name || '—'}</div>
            <span />
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Email')}
              <span>{t('Google · вход в кабинет')}</span>
            </div>
            <div style={{ fontFamily: 'JetBrains Mono', fontSize: 13 }}>{user?.email || '—'}</div>
            <span />
          </div>
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Сессия')}</h2>
          </div>
          <div className="set-row">
            <div className="lbl">
              {t('Выйти из кабинета')}
              <span>{t('завершить текущую SSO-сессию')}</span>
            </div>
            <span />
            <button className="btn btn-ghost btn-sm" onClick={onLogout}>
              {t('Выйти')}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   Billing page
   ───────────────────────────────────────────────────────────── */
function tierLabel(tr, tiers, i) {
  const from = i === 0 ? 0 : tiers[i - 1].upTo;
  if (tr.upTo == null) return t('от') + ' ' + fmt(from) + ' ₸';
  return fmt(from) + ' – ' + fmt(tr.upTo) + ' ₸';
}

function TopupModal({ onClose, onDone }) {
  const [amount, setAmount] = useState(10000);
  const [phase, setPhase] = useState('form'); // form | qr | paid | error
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [tu, setTu] = useState(null);
  const pollRef = useRef(null);

  useEffect(() => () => clearInterval(pollRef.current), []);

  async function create() {
    setBusy(true);
    setErr('');
    try {
      const r = await post('/api/billing/topup', { amount: Number(amount) });
      setTu(r);
      setPhase('qr');
      pollRef.current = setInterval(async () => {
        try {
          const s = await api('/api/billing/topup/' + r.topupId);
          if (s.status === 'paid') {
            clearInterval(pollRef.current);
            setPhase('paid');
            onDone();
          } else if (s.status === 'expired' || s.status === 'failed') {
            clearInterval(pollRef.current);
            setErr(t('Пополнение не завершено'));
            setPhase('error');
          }
        } catch {
          /* keep polling */
        }
      }, 3000);
    } catch (e) {
      setErr(e.message);
      setPhase('error');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div
      onClick={onClose}
      style={{
        position: 'fixed',
        inset: 0,
        background: 'rgba(15, 15, 16, 0.55)',
        display: 'grid',
        placeItems: 'center',
        zIndex: 200,
        padding: 24,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: '100%',
          maxWidth: 440,
          background: 'var(--paper)',
          border: '1px solid var(--ink)',
          borderRadius: 4,
        }}
      >
        <div className="panel-head" style={{ borderBottom: '1px solid var(--ink)' }}>
          <h2>{t('Пополнение баланса')}</h2>
          <div className="right">
            <a onClick={onClose} style={{ cursor: 'pointer' }}>✕ {t('Закрыть')}</a>
          </div>
        </div>
        <div style={{ padding: 22 }}>
          {phase === 'form' && (
            <>
              <label
                style={{
                  fontFamily: 'JetBrains Mono',
                  fontSize: 11,
                  letterSpacing: '0.04em',
                  textTransform: 'uppercase',
                  color: 'var(--mute)',
                }}
              >
                {t('Сумма пополнения (₸)')}
              </label>
              <input
                type="number"
                min="100"
                value={amount}
                onChange={(e) => setAmount(e.target.value)}
                style={{
                  width: '100%',
                  fontFamily: 'JetBrains Mono',
                  fontSize: 14,
                  padding: '12px 14px',
                  border: '1px solid var(--ink)',
                  background: 'var(--paper)',
                  borderRadius: 3,
                  outline: 'none',
                  margin: '8px 0 16px',
                }}
              />
              <button
                className="btn btn-ink"
                style={{ width: '100%' }}
                disabled={busy || Number(amount) < 100}
                onClick={create}
              >
                {busy ? '…' : t('Создать QR для оплаты')}
              </button>
            </>
          )}
          {phase === 'qr' && tu && (
            <div style={{ textAlign: 'center' }}>
              <div
                style={{
                  fontSize: 34,
                  fontFamily: 'var(--sans)',
                  fontWeight: 500,
                  letterSpacing: '-0.03em',
                  marginBottom: 8,
                }}
              >
                {fmt(tu.amount)} ₸
              </div>
              <p style={{ fontSize: 13.5, color: 'var(--mute)', marginBottom: 18 }}>
                {t('Откройте оплату через Kaspi — баланс зачислится автоматически после оплаты.')}
              </p>
              <a
                className="btn btn-ink"
                href={tu.qrToken}
                target="_blank"
                rel="noopener"
                style={{ width: '100%', justifyContent: 'center' }}
              >
                {t('Открыть оплату Kaspi')} →
              </a>
              <div
                style={{
                  marginTop: 16,
                  fontFamily: 'JetBrains Mono',
                  fontSize: 12,
                  color: 'var(--mute)',
                }}
              >
                {t('Ожидаем оплату…')}
              </div>
            </div>
          )}
          {phase === 'paid' && (
            <div style={{ textAlign: 'center', padding: '12px 0' }}>
              <div style={{ fontSize: 40, color: 'var(--ok)' }}>✓</div>
              <div style={{ fontWeight: 500, fontSize: 18, margin: '8px 0' }}>
                {t('Баланс успешно пополнен')}
              </div>
              <button className="btn btn-ink" style={{ marginTop: 12 }} onClick={onClose}>
                {t('Готово')}
              </button>
            </div>
          )}
          {phase === 'error' && (
            <div style={{ textAlign: 'center', padding: '12px 0' }}>
              <div
                style={{
                  color: 'var(--warn)',
                  fontFamily: 'JetBrains Mono',
                  fontSize: 13,
                  marginBottom: 14,
                }}
              >
                {err}
              </div>
              <button
                className="btn btn-ghost btn-sm"
                onClick={() => {
                  setPhase('form');
                  setErr('');
                }}
              >
                ← {t('Назад')}
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function BillingPage() {
  const [billing, setBilling] = useState(null);
  const [ledger, setLedger] = useState([]);
  const [topup, setTopup] = useState(false);
  const [loading, setLoading] = useState(true);

  const load = useCallback(async () => {
    try {
      setBilling(await api('/api/billing'));
      const { ledger } = await api('/api/billing/ledger');
      setLedger(ledger);
    } catch {
      /* ignore transient errors */
    } finally {
      setLoading(false);
    }
  }, []);
  useEffect(() => {
    load();
  }, [load]);

  if (loading || !billing) {
    return (
      <div className="page active">
        <div className="empty">{t('Загрузка…')}</div>
      </div>
    );
  }

  const tiers = billing.tiers || [];
  const curIdx = tiers.findIndex(
    (tr) => tr.upTo == null || billing.cumulativeTurnover <= tr.upTo,
  );
  const statusColor = billing.suspended
    ? 'var(--warn)'
    : billing.low
      ? '#b07a0c'
      : 'var(--ok)';
  const statusText = billing.suspended
    ? t('приём приостановлен')
    : billing.low
      ? t('низкий баланс')
      : t('активен');

  return (
    <div className="page active">
      <div className="page-head">
        <div className="title">
          <h1>
            <span className="it">{t('Биллинг')}</span> {t('и тариф.')}
          </h1>
          <p>{t('Баланс пополняется через Kaspi. Комиссия списывается с каждого успешного платежа.')}</p>
        </div>
        <div className="actions">
          <button
            className="btn btn-ink btn-sm"
            disabled={!billing.topupEnabled}
            title={billing.topupEnabled ? '' : t('Пополнение временно недоступно')}
            onClick={() => setTopup(true)}
          >
            + {t('Пополнить')}
          </button>
        </div>
      </div>

      <div className="content">
        <div className="panel">
          <div className="panel-head">
            <h2>{t('Баланс')}</h2>
            <div className="right" style={{ color: statusColor }}>
              ● {statusText}
            </div>
          </div>
          <div className="balance">
            <div className="k">{t('Текущий баланс')}</div>
            <div className="v" style={{ color: billing.suspended ? 'var(--warn)' : 'var(--ink)' }}>
              {fmt(billing.balance)}
              <span className="cur">₸</span>
            </div>
            <div className="sub">
              {billing.suspended
                ? t('Баланс в минусе — пополните, чтобы принимать платежи')
                : billing.low
                  ? t('Низкий баланс — рекомендуем пополнить')
                  : t('Достаточно для приёма платежей')}
            </div>
          </div>
          <div className="balance" style={{ borderBottom: 0 }}>
            <div className="k">{t('Накопленный оборот')}</div>
            <div className="v" style={{ fontSize: 28 }}>
              {fmt(billing.cumulativeTurnover)}
              <span className="cur" style={{ fontSize: 18 }}>₸</span>
            </div>
            <div className="tier-info">
              <span>
                {t('текущая ставка')} · <b>{billing.rate}%</b>
              </span>
              <span>
                {t('последнее списание')} ·{' '}
                <b>{billing.lastCommissionAt ? dateOf(billing.lastCommissionAt) : '—'}</b>
              </span>
            </div>
          </div>
        </div>

        <div className="panel">
          <div className="panel-head">
            <h2>{t('Тарифные уровни')}</h2>
            <span className="sub">· {t('ставка по обороту')}</span>
          </div>
          <table className="tbl">
            <thead>
              <tr>
                <th>{t('Оборот')}</th>
                <th>{t('Ставка')}</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {tiers.map((tr, i) => (
                <tr key={i} style={{ background: i === curIdx ? 'var(--paper-2)' : '' }}>
                  <td className="col-ref">{tierLabel(tr, tiers, i)}</td>
                  <td className="col-amt">{tr.rate}%</td>
                  <td>
                    {i === curIdx && <span className="stat-pill ok">{t('текущий')}</span>}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      <div className="content" style={{ gridTemplateColumns: '1fr', paddingTop: 0 }}>
        <div className="panel">
          <div className="panel-head">
            <h2>{t('История')}</h2>
            <span className="sub">· {ledger.length}</span>
          </div>
          {ledger.length === 0 ? (
            <div className="empty">
              <div className="glyph">∅</div>
              {t('Движений пока нет')}
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>{t('Дата')}</th>
                  <th>{t('Тип')}</th>
                  <th>{t('Сумма')}</th>
                  <th>{t('Баланс после')}</th>
                </tr>
              </thead>
              <tbody>
                {ledger.map((l, i) => {
                  const amt = Number(l.amount);
                  return (
                    <tr key={i}>
                      <td className="col-t">
                        {dateOf(l.created_at)} {timeOf(l.created_at)}
                      </td>
                      <td className="col-ref">
                        {l.type === 'topup'
                          ? t('Пополнение')
                          : l.type === 'commission'
                            ? t('Комиссия') + ' ' + (l.rate || '') + '%'
                            : t('Корректировка')}
                      </td>
                      <td
                        className="col-amt"
                        style={{ color: amt >= 0 ? 'var(--ok)' : 'var(--ink)' }}
                      >
                        {amt >= 0 ? '+' : ''}
                        {fmt(amt)} ₸
                      </td>
                      <td className="col-amt">{fmt(Number(l.balance_after))} ₸</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {topup && <TopupModal onClose={() => setTopup(false)} onDone={load} />}
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────
   App
   ───────────────────────────────────────────────────────────── */
function App() {
  const [page, setPage] = useState('overview');
  const [user, setUser] = useState(null);
  const [payments, setPayments] = useState([]);
  const [kaspi, setKaspi] = useState({ connected: false });
  const [modal, setModal] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const root = document.documentElement;
    root.style.setProperty('--acid', '#ff7a5b');
    root.style.setProperty('--acid-deep', '#e6573a');
  }, []);

  const loadPayments = useCallback(async () => {
    try {
      const { payments } = await api('/api/payments?limit=200');
      setPayments(payments);
    } catch {
      /* ignore transient errors */
    }
  }, []);

  const loadKaspi = useCallback(async () => {
    try {
      setKaspi(await api('/api/auth/status'));
    } catch {
      setKaspi({ connected: false });
    }
  }, []);

  useEffect(() => {
    (async () => {
      try {
        const me = await api('/api/account/me');
        setUser({ ...me.user, tenantName: me.tenant?.name });
      } catch {
        return; // api() redirected on 401
      }
      await Promise.all([loadPayments(), loadKaspi()]);
      setLoading(false);
    })();
  }, [loadPayments, loadKaspi]);

  // Poll payments while the cabinet is open.
  useEffect(() => {
    const id = setInterval(loadPayments, 5000);
    return () => clearInterval(id);
  }, [loadPayments]);

  async function logout() {
    try {
      await post('/api/account/logout');
    } catch {
      /* ignore */
    }
    location.href = '/login.html';
  }

  function reloadAll() {
    loadPayments();
    loadKaspi();
  }

  if (loading) {
    return (
      <div style={{ display: 'grid', placeItems: 'center', minHeight: '100vh' }}>
        <span style={{ fontFamily: 'JetBrains Mono', color: 'var(--mute)' }}>{t('Загрузка…')}</span>
      </div>
    );
  }

  return (
    <div className="app-shell">
      <Sidebar
        page={page}
        setPage={setPage}
        user={user}
        opsCount={payments.length}
        onLogout={logout}
      />
      <main className="main">
        <Topbar page={page} kaspi={kaspi} onReload={reloadAll} />
        {page === 'overview' && (
          <Overview
            user={user}
            payments={payments}
            kaspi={kaspi}
            setPage={setPage}
            openModal={() => setModal(true)}
          />
        )}
        {page === 'ops' && <OpsPage payments={payments} openModal={() => setModal(true)} />}
        {page === 'kaspi' && <KaspiPage kaspi={kaspi} reloadKaspi={loadKaspi} />}
        {page === 'keys' && <KeysPage />}
        {page === 'billing' && <BillingPage />}
        {page === 'webhooks' && <WebhooksPage />}
        {page === 'settings' && <SettingsPage user={user} onLogout={logout} />}
      </main>
      {modal && (
        <PaymentModal onClose={() => setModal(false)} onDone={loadPayments} />
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
