/* global React, Icon, Pill, DirChip, Avatar, Money, Card, Overline, Dropdown, Button */
// Schools.ArtemenkoCRM — Счета и Финансы

const fmtRub = (n) => new Intl.NumberFormat('ru-RU').format(n) + ' ₽';

// ---- KPI tile ----
function KpiTile({ overline, value, sub, accent }) {
  const color = accent === 'green' ? 'var(--green)' : accent === 'amber' ? 'var(--amber)' : accent === 'coral' ? 'var(--coral)' : 'var(--brand)';
  return (
    <Card pad={20}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ width: 9, height: 9, borderRadius: '50%', background: color }} />
        <Overline>{overline}</Overline>
      </div>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 26, fontWeight: 700, marginTop: 12, color: 'var(--text)' }}><span key={value} className="om-pop" style={{ display: 'inline-block' }}>{value}</span></div>
      {sub && <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 4 }}>{sub}</div>}
    </Card>
  );
}

// ---- Row action kebab menu (portal — always above the table) ----
function RowMenu({ items }) {
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const ref = React.useRef(null);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    if (ref.current) setRect(ref.current.getBoundingClientRect());
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && menuRef.current && !menuRef.current.contains(e.target)) setOpen(false); };
    const onScroll = () => setOpen(false);
    document.addEventListener('mousedown', onDoc);
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => { document.removeEventListener('mousedown', onDoc); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onScroll); };
  }, [open]);
  const menuW = 210;
  const left = rect ? Math.min(rect.right - menuW, window.innerWidth - menuW - 8) : 0;
  const belowRoom = rect ? window.innerHeight - rect.bottom : 999;
  const flipUp = belowRoom < 260;
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
      <button onClick={e => { e.stopPropagation(); setOpen(o => !o); }} title="Ещё" style={{ width: 30, height: 30, borderRadius: 6, border: '1px solid var(--border)', background: open ? 'var(--bg-soft)' : 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
        <Icon name="more-horizontal" size={17} />
      </button>
      {open && rect && ReactDOM.createPortal(
        <div ref={menuRef} className="om-fade-in" style={{
          position: 'fixed', left, width: menuW, zIndex: 9999,
          top: flipUp ? undefined : rect.bottom + 4, bottom: flipUp ? (window.innerHeight - rect.top + 4) : undefined,
          background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', padding: 4,
        }}>
          {items.map((it, i) => it.sep ? <div key={i} style={{ height: 1, background: 'var(--border)', margin: '4px 0' }} /> : (
            <button key={i} onClick={e => { e.stopPropagation(); setOpen(false); it.onClick(); }} style={{
              display: 'flex', alignItems: 'center', gap: 9, width: '100%', textAlign: 'left', padding: '8px 10px',
              border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-sm)', background: 'transparent',
              color: it.danger ? 'var(--coral)' : 'var(--text)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 500,
            }}
            onMouseEnter={e => { e.currentTarget.style.background = it.danger ? 'var(--coral-bg)' : 'var(--bg-soft)'; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
              <Icon name={it.icon} size={15} style={{ color: it.danger ? 'var(--coral)' : 'var(--text-dim)' }} />{it.label}
            </button>
          ))}
        </div>,
        document.body
      )}
    </div>
  );
}

// ---- KPI cards row (Счета only) ----
function InvoiceKpis() {
  const { INVOICES } = window.CRM_DATA;
  const pending = INVOICES.filter(i => i.status === 'pending' || i.status === 'overdue');
  const pendingSum = pending.reduce((a, b) => a + b.amount, 0);
  const paidMonth = INVOICES.filter(i => i.status === 'paid').reduce((a, b) => a + b.amount, 0);
  const overdueSum = INVOICES.filter(i => i.status === 'overdue').reduce((a, b) => a + b.amount, 0);
  // Название текущего месяца из реальной «сегодня» (не хардкод «май»). Именит. падеж — как «за май».
  const curMonth = ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'][(+((window.APP_TODAY_ISO || '2026-05-31').slice(5, 7))) - 1];
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20, marginBottom: 22 }}>
      <KpiTile overline="Ожидают оплаты" value={pending.length + ' шт.'} sub={'на ' + fmtRub(pendingSum)} accent="amber" />
      <KpiTile overline={'Оплачено за ' + curMonth} value={fmtRub(paidMonth)} sub={INVOICES.filter(i=>i.status==='paid').length + ' платежей'} accent="green" />
      <KpiTile overline="Просрочено" value={fmtRub(overdueSum)} sub={INVOICES.filter(i=>i.status==='overdue').length + ' счёт(а)'} accent="coral" />
    </div>
  );
}

// ============================ INVOICES TAB ============================
function InvoicesTab({ onAddInvoice, onToast, onUpdateClientBalance, onConfirm }) {
  const { INVOICES, INVOICE_TYPE_SHORT, CLIENTS } = window.CRM_DATA;
  const [status, setStatus] = React.useState(null);
  const [q, setQ] = React.useState('');
  const [sel, setSel] = React.useState([]);
  const [, force] = React.useReducer(x => x + 1, 0);

  const statusOpts = [{ value: null, label: 'Все статусы' },
    { value: 'paid', label: 'Оплачен' }, { value: 'pending', label: 'Ожидает' },
    { value: 'overdue', label: 'Просрочен' }, { value: 'cancelled', label: 'Отменён' }, { value: 'refund', label: 'Возврат' }];

  const rows = INVOICES.filter(inv => {
    if (status && inv.status !== status) return false;
    if (q && !(`${inv.id} ${inv.parent} ${inv.student} ${inv.desc}`.toLowerCase().includes(q.toLowerCase()))) return false;
    return true;
  });

  const clientOf = (inv) => CLIENTS.find(c => c.id === inv.clientId);
  const toggle = (id) => setSel(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);
  const allSel = rows.length > 0 && rows.every(r => sel.includes(r.id));
  const toggleAll = () => setSel(allSel ? [] : rows.map(r => r.id));

  const markPaid = async (inv, method) => {
    // Реальная оплата на бэкенде: платёж → пополнение баланса → пересчёт долга (FIFO).
    try {
      const res = await window.apiCall('/invoices/' + inv.id + '/pay', {
        method: 'POST', body: JSON.stringify({ method: method || inv.method || undefined }),
      });
      inv.status = 'paid'; inv.method = method || inv.method || 'СБП';
      const c = clientOf(inv);
      if (c) c.balance = Math.round((res.balanceKopecks || 0) / 100); // баланс из ответа сервера
      onToast('Счёт #' + inv.id + ' оплачен' + (c ? ' · баланс пополнен' : ''));
      force();
    } catch (e) {
      onToast('Не удалось оплатить счёт #' + inv.id + ': ' + (e.message || 'ошибка'));
    }
  };
  const cancel = (inv) => { inv.status = 'cancelled'; inv.method = null; onToast('Счёт #' + inv.id + ' отменён'); force(); };
  const refund = (inv) => { inv.status = 'refund'; onToast('Оформлен возврат по счёту #' + inv.id); force(); };
  const copyLink = (inv) => { if (navigator.clipboard) navigator.clipboard.writeText('https://pay.artemenko.crm/i/' + inv.id).catch(()=>{}); onToast('Ссылка на оплату счёта #' + inv.id + ' скопирована'); };

  const bulkCancel = () => {
    const targets = INVOICES.filter(i => sel.includes(i.id) && (i.status === 'pending' || i.status === 'overdue'));
    if (!targets.length) { onToast('Среди выбранных нет счетов, которые можно отменить'); return; }
    onConfirm({
      title: 'Отменить выбранные счета?',
      body: `Будет отменено счетов: ${targets.length}. Отменённые счета не считаются оплаченными.`,
      ok: 'Отменить (' + targets.length + ')', danger: true,
      onOk: () => { targets.forEach(i => { i.status = 'cancelled'; i.method = null; }); setSel([]); force(); onToast('Отменено счетов: ' + targets.length); },
    });
  };
  const bulkDelete = () => {
    onConfirm({
      title: 'Удалить выбранные счета?',
      body: `Будет удалено счетов: ${sel.length}. Действие необратимо — счета нельзя будет восстановить.`,
      ok: 'Удалить (' + sel.length + ')', danger: true,
      onOk: () => { sel.forEach(id => { const i = INVOICES.findIndex(x => x.id === id); if (i !== -1) INVOICES.splice(i, 1); }); const n = sel.length; setSel([]); force(); onToast('Удалено счетов: ' + n); },
    });
  };

  const exportCsv = () => {
    const head = ['#', 'Родитель', 'Ученик', 'Тип', 'Сумма', 'Способ', 'Статус', 'Дата'];
    const body = rows.map(r => [r.id, r.parent, r.student, INVOICE_TYPE_SHORT[r.type] || r.type, r.amount, r.method || '', r.status, r.date]);
    const csv = [head, ...body].map(line => line.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n');
    const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
    const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'invoices.csv'; a.click();
    onToast('База счетов выгружена в CSV');
  };
  const importRef = React.useRef(null);
  const importCsv = (e) => { const f = e.target.files && e.target.files[0]; if (f) onToast('Загружен файл: ' + f.name + ' (демо)'); e.target.value = ''; };

  const th = { textAlign: 'left', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '0 16px 13px', whiteSpace: 'nowrap' };
  const td = { padding: '12px 16px', borderTop: '1px solid var(--border)', fontSize: 14, color: 'var(--text)', verticalAlign: 'middle' };
  const dim = { color: 'var(--text-dim)' };
  const Check = ({ on, onClick }) => (
    <button onClick={e => { e.stopPropagation(); onClick(); }} style={{ width: 18, height: 18, borderRadius: 4, border: '1.5px solid ' + (on ? 'var(--brand)' : 'var(--border-strong)'), background: on ? 'var(--brand)' : 'var(--bg-card)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none', padding: 0 }}>
      {on && <Icon name="check" size={12} strokeWidth={3} style={{ color: '#fff' }} />}
    </button>
  );

  return (
    <div>
      <InvoiceKpis />
      {/* Toolbar */}
      <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10, marginBottom: 16 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', padding: '8px 12px', width: 240 }}>
          <Icon name="search" size={16} style={{ color: 'var(--text-dim)' }} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Поиск по счетам…" style={{ border: 'none', outline: 'none', background: 'transparent', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 13.5, color: 'var(--text)' }} />
        </div>
        <Dropdown value={status} onChange={setStatus} options={statusOpts} placeholder="Все статусы" width={160} searchable />
        <div style={{ flex: 1 }} />
        <Button icon="add-student" onClick={onAddInvoice}>Выставить счёт</Button>
      </div>

      {/* Bulk bar */}
      {sel.length > 0 && (
        <div className="om-fade-in" style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', marginBottom: 14, background: 'var(--brand-soft)', border: '1px solid #CBD9F0', borderRadius: 'var(--radius-md)' }}>
          <span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--brand-ink)' }}>Выбрано: {sel.length}</span>
          <div style={{ flex: 1 }} />
          <Button variant="secondary" size="sm" icon="x-circle" onClick={bulkCancel}>Отменить</Button>
          <Button variant="danger" size="sm" icon="close-tile" onClick={bulkDelete}>Удалить</Button>
          <button onClick={() => setSel([])} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--text-dim)', fontSize: 13, fontWeight: 600 }}>Снять</button>
        </div>
      )}

      <Card pad={0} style={{ overflow: 'hidden' }}>
        {rows.length > 0 ? (
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 980 }}>
            <thead><tr>
              <th style={{ ...th, paddingTop: 16, width: 20 }}><Check on={allSel} onClick={toggleAll} /></th>
              <th style={{ ...th, paddingTop: 16, width: 44 }}>#</th>
              <th style={{ ...th, paddingTop: 16 }}>Родитель / Ученик</th>
              <th style={{ ...th, paddingTop: 16 }}>Тип</th>
              <th style={{ ...th, paddingTop: 16, textAlign: 'right' }}>Сумма</th>
              <th style={{ ...th, paddingTop: 16 }}>Способ</th>
              <th style={{ ...th, paddingTop: 16 }}>Статус</th>
              <th style={{ ...th, paddingTop: 16 }}>Дата</th>
              <th style={{ ...th, paddingTop: 16, textAlign: 'right' }}>Действия</th>
            </tr></thead>
            <tbody key={`${status}|${q}`}>
              {rows.map((inv, ri) => {
                const c = clientOf(inv);
                const open = inv.status === 'pending' || inv.status === 'overdue';
                const menu = [
                  { icon: 'link', label: 'Копировать ссылку', onClick: () => copyLink(inv) },
                  { icon: 'file-down', label: 'Скачать PDF', onClick: () => onToast('PDF счёта #' + inv.id + ' сформирован (демо)') },
                  { icon: 'send', label: 'Отправить клиенту', onClick: () => onToast('Счёт #' + inv.id + ' отправлен клиенту (демо)') },
                ];
                if (inv.status === 'paid') menu.push({ sep: true }, { icon: 'rotate-ccw', label: 'Оформить возврат', onClick: () => refund(inv) });
                menu.push({ sep: true }, { icon: 'close-tile', label: 'Удалить', danger: true, onClick: () => onConfirm({ title: 'Удалить счёт #' + inv.id + '?', body: 'Действие окончательно — счёт нельзя будет восстановить.', ok: 'Удалить', danger: true, onOk: () => { const i = window.CRM_DATA.INVOICES.findIndex(x => x.id === inv.id); if (i !== -1) window.CRM_DATA.INVOICES.splice(i, 1); onToast('Счёт #' + inv.id + ' удалён'); force(); } }) });
                return (
                <tr key={inv.id} className="row-hover om-row-in" style={{ animationDelay: Math.min(ri * 22, 280) + 'ms', cursor: sel.length ? 'pointer' : 'default', background: sel.includes(inv.id) ? 'var(--bg-soft)' : undefined }} onClick={() => { if (sel.length) toggle(inv.id); }}>
                  <td style={td}><Check on={sel.includes(inv.id)} onClick={() => toggle(inv.id)} /></td>
                  <td style={{ ...td, ...dim, fontFamily: 'var(--font-mono)', fontSize: 12.5 }}>{inv.id}</td>
                  <td style={td}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      {c ? <Avatar animal={c.animal} tone={c.tone} size={30} /> : <span style={{ width: 30, height: 30, borderRadius: '50%', background: 'var(--bg-soft)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-dim)', flex: 'none' }}><Icon name="user" size={15} /></span>}
                      <div style={{ minWidth: 0 }}>
                        <div style={{ fontWeight: 600 }}>{inv.student}</div>
                        <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{inv.parent || '—'}</div>
                      </div>
                    </div>
                  </td>
                  <td style={{ ...td, fontSize: 13 }}><span style={{ padding: '2px 8px', borderRadius: 'var(--radius-xs)', background: 'var(--bg-soft)', color: 'var(--text-secondary)', fontWeight: 600, fontSize: 12 }}>{INVOICE_TYPE_SHORT[inv.type] || inv.type}</span></td>
                  <td style={{ ...td, textAlign: 'right' }}><Money value={inv.amount} style={{ fontSize: 14 }} /></td>
                  <td style={{ ...td, ...(inv.method ? { color: 'var(--text-secondary)' } : dim), fontSize: 13 }}>{inv.method || '—'}</td>
                  <td style={td}><Pill kind={inv.status} /></td>
                  <td style={{ ...td, ...dim, fontSize: 13, whiteSpace: 'nowrap' }}>{inv.date}</td>
                  <td style={td} onClick={e => e.stopPropagation()}>
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 }}>
                      {open && <button onClick={() => markPaid(inv)} title="Отметить оплаченным" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '6px 10px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--green)', background: 'var(--green-bg)', color: 'var(--green-ink)', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', transition: 'background 140ms var(--ease), transform 120ms var(--ease)' }} onMouseEnter={e => { e.currentTarget.style.background = '#CFEAD9'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--green-bg)'; }} onMouseDown={e => { e.currentTarget.style.transform = 'translateY(1px)'; }} onMouseUp={e => { e.currentTarget.style.transform = 'none'; }}><Icon name="check" size={13} strokeWidth={2.5} />Оплачен</button>}
                      {open && <button onClick={() => cancel(inv)} title="Отменить счёт" style={{ padding: '6px 10px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--coral-bg)', background: 'var(--bg-card)', color: 'var(--coral)', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', transition: 'background 140ms var(--ease)' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--coral-bg)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}>Отменить</button>}
                      <RowMenu items={menu} />
                    </div>
                  </td>
                </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        ) : (
          <div style={{ padding: '48px 24px', textAlign: 'center', color: 'var(--text-dim)', fontSize: 14 }}>Счетов по фильтру нет</div>
        )}
      </Card>
    </div>
  );
}

// ============================ EXPENSES TAB ============================
function ExpensesTab({ onToast, onAddExpense, onNavSettings, onPayout, vMonth, setVMonth }) {
  const { EXPENSES, PAYOUTS } = window.CRM_DATA;
  const PAYOUT_HISTORY = window.CRM_DATA.PAYOUT_HISTORY || [];
  // Авто-расход: зарплата сотрудникам за текущий месяц = сумма выплат, ПРОВЕДЁННЫХ в этом месяце.
  const ABBR = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
  const MON_FULL = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
  const todayISO = window.APP_TODAY_ISO || '2026-05-31';
  const curMonth = +todayISO.slice(5, 7) - 1, curYear = +todayISO.slice(0, 4);
  const parsePay = (s) => { const m = String(s || '').toLowerCase().match(/(\d{1,2})\s+([а-я]+)\s+(\d{4})/); if (!m) return null; const mi = ABBR.findIndex(x => m[2].startsWith(x)); return mi < 0 ? null : { month: mi, year: +m[3] }; };
  // Расход на зарплату относим к МЕСЯЦУ ПЕРИОДА начисления (за апрель → в апреле), а не к дате выплаты.
  // periodStart (ISO) — точный источник; иначе пробуем разобрать подпись периода; иначе дату выплаты.
  const payoutMY = (p) => {
    if (p.expenseAttr === 'payment') { const d = parsePay(p.payDate); if (d) return d; } // учли в месяце выплаты
    if (p.periodStart) { return { month: +p.periodStart.slice(5, 7) - 1, year: +p.periodStart.slice(0, 4) }; }
    const m = String(p.period || '').toLowerCase().match(/([а-я]{3,})\s*(\d{4})?/);
    if (m) { const mi = ABBR.findIndex(x => m[1].startsWith(x)); if (mi >= 0) return { month: mi, year: m[2] ? +m[2] : curYear }; }
    return parsePay(p.payDate) || { month: -1, year: -1 };
  };
  const salaryPayouts = PAYOUT_HISTORY.filter(p => { if (p.status !== 'paid' || p.voidedAt) return false; const d = payoutMY(p); return d.month === vMonth && d.year === curYear; });
  const salaryThisMonth = salaryPayouts.reduce((a, b) => a + b.amount, 0);
  const monthName = MON_FULL[vMonth];
  const [openRows, setOpenRows] = React.useState({}); // раскрытые строки расходов ('salary' | 'e'+id)
  const toggleRow = (k) => setOpenRows(o => ({ ...o, [k]: !o[k] }));
  const [incomeBy, setIncomeBy] = React.useState('students'); // разбивка дохода: students | teachers
  const fmtDT = (iso) => { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d)) return iso; return `${d.getDate()} ${ABBR[d.getMonth()]} ${d.getFullYear()}, ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
  const more = { display: 'inline-flex', alignItems: 'center', gap: 3, marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', fontFamily: 'var(--font-sans)', fontSize: 11.5, fontWeight: 600, verticalAlign: 'middle' };

  const expTotal = EXPENSES.reduce((a, b) => a + b.amount, 0) + salaryThisMonth;
  const payoutPending = PAYOUTS.filter(p => p.status === 'pending' && !p.voidedAt).reduce((a, b) => a + b.amount, 0);
  // Доход за месяц — с СЕРВЕРА (`GET /reports/income`): занятия месяца, покрытые деньгами по FIFO.
  // Раньше складывались занятия из client.__lessons, а они грузятся только при открытии карточки
  // ученика → в Финансах доход всегда был 0 (и зависел от того, куда пользователь успел кликнуть).
  const [income, setIncome] = React.useState(null);
  const [incomeLoading, setIncomeLoading] = React.useState(true);
  const [incomeError, setIncomeError] = React.useState(false);
  React.useEffect(() => {
    if (!window.fetchIncome) { setIncomeLoading(false); return; }
    let alive = true;
    setIncomeLoading(true); setIncomeError(false);
    const mm = String(vMonth + 1).padStart(2, '0');
    const last = new Date(curYear, vMonth + 1, 0).getDate();
    window.fetchIncome(`${curYear}-${mm}-01`, `${curYear}-${mm}-${String(last).padStart(2, '0')}`)
      .then((r) => { if (alive) { setIncome(r); setIncomeLoading(false); } })
      .catch(() => { if (alive) { setIncome(null); setIncomeError(true); setIncomeLoading(false); } });
    return () => { alive = false; };
  }, [vMonth, curYear]);
  const incomeTotal = income ? income.total : 0;
  const incomeByStudent = income ? income.byStudent : []; // для подписи плитки; таблицы строятся из income.rows
  const profit = incomeTotal - expTotal;
  const th = { textAlign: 'left', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '0 16px 13px', whiteSpace: 'nowrap' };
  const td = { padding: '12px 16px', borderTop: '1px solid var(--border)', fontSize: 14, color: 'var(--text)', verticalAlign: 'middle' };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
        <KpiTile overline={`Расходы за ${monthName.toLowerCase()}`} value={fmtRub(expTotal)} sub={`${EXPENSES.length} операц.${salaryThisMonth ? ' + зарплаты' : ''}`} accent="coral" />
        <KpiTile overline={`Доход за ${monthName.toLowerCase()}`}
          value={incomeLoading ? <span className="om-skel" style={{ display: 'inline-block', width: 108, height: 26, borderRadius: 6, verticalAlign: 'middle', background: 'var(--bg-soft)' }} /> : (incomeError ? '—' : fmtRub(incomeTotal))}
          sub={incomeLoading ? 'считаем по занятиям…' : (incomeError ? 'не удалось загрузить' : `${incomeByStudent.length} учеников · оплаченные занятия`)} accent="green" />
        <KpiTile overline={`Прибыль за ${monthName.toLowerCase()}`}
          value={incomeLoading ? <span className="om-skel" style={{ display: 'inline-block', width: 108, height: 26, borderRadius: 6, verticalAlign: 'middle', background: 'var(--bg-soft)' }} /> : (incomeError ? '—' : fmtRub(profit))}
          sub="доход − расходы" accent={incomeLoading || profit >= 0 ? 'green' : 'coral'} />
      </div>

      <Card pad={0} style={{ overflow: 'hidden' }}>
        <div style={{ padding: '18px 16px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <Overline>Расходы школы</Overline>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <Button variant="secondary" size="sm" icon="plus" onClick={onAddExpense}>Добавить расход</Button>
          </div>
        </div>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead><tr><th style={th}>Категория</th><th style={th}>Назначение</th><th style={th}>Способ</th><th style={{ ...th, textAlign: 'right' }}>Сумма</th><th style={{ ...th, textAlign: 'right' }}>Дата</th></tr></thead>
          <tbody>
            {salaryThisMonth > 0 && (
              <React.Fragment>
              <tr style={{ background: 'var(--bg-soft)' }}>
                <td style={td}><span style={{ padding: '2px 8px', borderRadius: 'var(--radius-xs)', background: 'var(--brand-soft)', color: 'var(--brand-ink)', fontWeight: 600, fontSize: 12 }}>Зарплата</span></td>
                <td style={td}>Зарплата сотрудникам
                  <span title="Считается автоматически из выплат за период начисления" style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginLeft: 8, padding: '1px 7px', borderRadius: 'var(--radius-pill)', fontSize: 11, fontWeight: 600, background: 'var(--bg-card)', border: '1px solid var(--border)', color: 'var(--text-dim)', verticalAlign: 'middle' }}><Icon name="zap" size={11} />авто</span>
                  <span style={{ marginLeft: 8, fontSize: 12.5, color: 'var(--text-dim)' }}>· {salaryPayouts.length} выпл. за {monthName.toLowerCase()}</span>
                  <button onClick={() => toggleRow('salary')} style={more}>Подробнее<Icon name="chevron-down" size={12} style={{ transform: openRows.salary ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur) var(--ease)' }} /></button>
                </td>
                <td style={{ ...td, color: 'var(--text-secondary)', fontSize: 13 }}>—</td>
                <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--coral)' }}>−{new Intl.NumberFormat('ru-RU').format(salaryThisMonth)} ₽</td>
                <td style={{ ...td, textAlign: 'right', color: 'var(--text-dim)', fontSize: 13, whiteSpace: 'nowrap' }}>{monthName}</td>
              </tr>
              {openRows.salary && (
                <tr style={{ background: 'var(--bg-soft)' }}>
                  <td colSpan={5} style={{ padding: '0 16px 14px' }}>
                    <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', overflow: 'hidden', background: 'var(--bg-card)' }}>
                      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '9px 12px', borderBottom: '1px solid var(--border)' }}>Кому и за что · {monthName} {curYear}</div>
                      {salaryPayouts.map((p, i) => (
                        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '9px 12px', borderTop: i ? '1px solid var(--border)' : 'none' }}>
                          <span style={{ flex: 1, minWidth: 0 }}>
                            <span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--text)' }}>{p.teacher}</span>
                            <span style={{ fontSize: 12, color: 'var(--text-dim)', marginLeft: 8 }}>период: {p.period || '—'}</span>
                          </span>
                          <span style={{ fontSize: 11.5, color: 'var(--text-dim)', whiteSpace: 'nowrap' }} title="Выплачено · Сформировано">{p.payDate || '—'}{p.createdAt ? ' · сформ. ' + fmtDT(p.createdAt) : ''}</span>
                          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: 'var(--coral)', flex: 'none', minWidth: 90, textAlign: 'right' }}>−{new Intl.NumberFormat('ru-RU').format(p.amount)} ₽</span>
                        </div>
                      ))}
                    </div>
                  </td>
                </tr>
              )}
              </React.Fragment>
            )}
            {EXPENSES.map(e => { const ek = 'e' + e.id; const op = !!openRows[ek]; return (
              <React.Fragment key={e.id}>
              <tr className="row-hover">
                <td style={td}><span style={{ padding: '2px 8px', borderRadius: 'var(--radius-xs)', background: 'var(--bg-soft)', color: 'var(--text-secondary)', fontWeight: 600, fontSize: 12 }}>{e.category}</span></td>
                <td style={td}>
                  <span>{e.title}{e.recurring && <span title="Повторяющийся расход" style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginLeft: 8, padding: '1px 7px', borderRadius: 'var(--radius-pill)', fontSize: 11, fontWeight: 600, background: 'var(--brand-soft)', color: 'var(--brand-ink)', verticalAlign: 'middle' }}><Icon name="repeat" size={11} />{({day:'день',week:'неделя',month:'месяц',year:'год'})[e.recurPeriod] || 'повтор'}</span>}{e.description && <button onClick={() => toggleRow(ek)} style={more}>Подробнее<Icon name="chevron-down" size={12} style={{ transform: op ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur) var(--ease)' }} /></button>}</span>
                  {e.description && !op && <div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 360 }}>{e.description}</div>}
                </td>
                <td style={{ ...td, color: 'var(--text-secondary)', fontSize: 13 }}>{e.method}</td>
                <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--coral)' }}>−{new Intl.NumberFormat('ru-RU').format(e.amount)} ₽</td>
                <td style={{ ...td, textAlign: 'right', color: 'var(--text-dim)', fontSize: 13, whiteSpace: 'nowrap' }}>{e.date}</td>
              </tr>
              {e.description && op && (
                <tr>
                  <td colSpan={5} style={{ padding: '0 16px 14px' }}>
                    <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-soft)', padding: '11px 13px', fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{e.description}</div>
                  </td>
                </tr>
              )}
              </React.Fragment>
            ); })}
          </tbody>
        </table>
      </Card>

      {/* Итог за месяц: по преподавателям и по ученикам. Строка раскрывается в занятия —
          видно, сколько каждое принесло: доход (цена занятия) − себестоимость (деньги преподу).
          NB: «расход» здесь — СЕБЕСТОИМОСТЬ занятий этого месяца (ставка на дату + надбавка, а у уже
          выплаченных — факт из снимка). Выплаченная за месяц зарплата — в «Расходах школы» выше:
          это касса (когда деньги ушли), а не за какие занятия. */}
      {[
        { key: 'teachers', title: 'Итог по преподавателям', col: 'Преподаватель', groupBy: (l) => l.teacher, subCol: 'Ученик', subOf: (l) => l.student },
        { key: 'students', title: 'Итог по ученикам', col: 'Ученик', groupBy: (l) => l.student, subCol: 'Преподаватель', subOf: (l) => l.teacher },
      ].map(cfg => {
        const rows = (income && income.rows) || [];
        const groups = {};
        rows.forEach(l => { const k = cfg.groupBy(l) || '—'; (groups[k] || (groups[k] = [])).push(l); });
        const list = Object.entries(groups).map(([name, ls]) => ({
          name, lessons: ls,
          income: ls.reduce((a, l) => a + l.income, 0),
          cost: ls.reduce((a, l) => a + l.cost, 0),
        })).map(g => ({ ...g, profit: g.income - g.cost })).sort((a, b) => b.income - a.income);
        const tot = list.reduce((a, g) => ({ income: a.income + g.income, cost: a.cost + g.cost }), { income: 0, cost: 0 });
        return (
          <Card key={cfg.key} pad={0} style={{ overflow: 'hidden' }}>
            <div style={{ padding: '18px 16px 14px' }}>
              <Overline>{cfg.title} · {monthName.toLowerCase()}</Overline>
            </div>
            <table style={{ width: '100%', borderCollapse: 'collapse' }}>
              <thead><tr>
                <th style={th}>{cfg.col}</th>
                <th style={{ ...th, textAlign: 'right' }}>Доход</th>
                <th style={{ ...th, textAlign: 'right' }}>Расход (преподу)</th>
                <th style={{ ...th, textAlign: 'right' }}>Прибыль</th>
              </tr></thead>
              <tbody>
                {incomeLoading ? [0, 1, 2].map(i => (
                  <tr key={'sk' + i}>{[0, 1, 2, 3].map(j => (
                    <td key={j} style={{ ...td, textAlign: j ? 'right' : 'left' }}>
                      <span className="om-skel" style={{ display: 'inline-block', width: j ? 72 : 180, height: 14, borderRadius: 4, background: 'var(--bg-soft)' }} />
                    </td>
                  ))}</tr>
                )) : list.length === 0 ? (
                  <tr><td colSpan={4} style={{ ...td, color: 'var(--text-dim)', fontSize: 13 }}>За {monthName.toLowerCase()} нет оплаченных занятий.</td></tr>
                ) : list.map(g => {
                  const rk = cfg.key + ':' + g.name;
                  const op = !!openRows[rk];
                  return (
                    <React.Fragment key={rk}>
                      <tr className="row-hover" onClick={() => toggleRow(rk)} style={{ cursor: 'pointer' }}>
                        <td style={{ ...td, fontWeight: 500 }}>
                          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
                            <Icon name="chevron-down" size={14} style={{ color: 'var(--text-dim)', flex: 'none', transform: op ? 'none' : 'rotate(-90deg)', transition: 'transform var(--dur) var(--ease)' }} />
                            {g.name}
                            <span style={{ fontSize: 11.5, color: 'var(--text-dim)', fontWeight: 500 }}>{g.lessons.length} зан.</span>
                          </span>
                        </td>
                        <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--green)' }}>+{fmtNum(g.income)} ₽</td>
                        <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 600, color: g.cost ? 'var(--coral)' : 'var(--text-dim)' }}>{g.cost ? '−' + fmtNum(g.cost) + ' ₽' : '—'}</td>
                        <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: g.profit >= 0 ? 'var(--green)' : 'var(--coral)' }}>{g.profit >= 0 ? '+' : '−'}{fmtNum(Math.abs(g.profit))} ₽</td>
                      </tr>
                      {op && (
                        <tr>
                          <td colSpan={4} style={{ padding: '0 16px 14px' }}>
                            <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-soft)', overflow: 'hidden' }}>
                              <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                                <thead><tr>
                                  <th style={{ ...th, padding: '10px 12px 8px', fontSize: 11 }}>Занятие</th>
                                  <th style={{ ...th, padding: '10px 12px 8px', fontSize: 11 }}>{cfg.subCol}</th>
                                  <th style={{ ...th, padding: '10px 12px 8px', fontSize: 11, textAlign: 'right' }}>Доход</th>
                                  <th style={{ ...th, padding: '10px 12px 8px', fontSize: 11, textAlign: 'right' }}>Преподу</th>
                                  <th style={{ ...th, padding: '10px 12px 8px', fontSize: 11, textAlign: 'right' }}>Прибыль</th>
                                </tr></thead>
                                <tbody>
                                  {g.lessons.map((l, i) => (
                                    <tr key={l.id} className="om-row-in" style={{ animationDelay: Math.min(i * 18, 240) + 'ms' }}>
                                      <td style={{ ...td, padding: '9px 12px', fontSize: 13, whiteSpace: 'nowrap' }}>{l.dateLabel}{l.time ? ', ' + l.time : ''}</td>
                                      <td style={{ ...td, padding: '9px 12px', fontSize: 13, color: 'var(--text-secondary)' }}>{cfg.subOf(l)}</td>
                                      <td style={{ ...td, padding: '9px 12px', fontSize: 13, textAlign: 'right', fontFamily: 'var(--font-mono)', color: 'var(--green)' }}>+{fmtNum(l.income)} ₽</td>
                                      <td style={{ ...td, padding: '9px 12px', fontSize: 13, textAlign: 'right', fontFamily: 'var(--font-mono)', color: 'var(--coral)' }}>
                                        −{fmtNum(l.cost)} ₽
                                        {l.costFromPayout && <span title="Сумма из снимка выплаты — столько преподаватель уже получил" style={{ marginLeft: 5, fontSize: 10, color: 'var(--text-dim)' }}>факт</span>}
                                      </td>
                                      <td style={{ ...td, padding: '9px 12px', fontSize: 13, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: (l.profit >= 0) ? 'var(--green)' : 'var(--coral)' }}>{l.profit >= 0 ? '+' : '−'}{fmtNum(Math.abs(l.profit))} ₽</td>
                                    </tr>
                                  ))}
                                </tbody>
                              </table>
                            </div>
                          </td>
                        </tr>
                      )}
                    </React.Fragment>
                  );
                })}
                {!incomeLoading && list.length > 0 && (
                  <tr style={{ background: 'var(--bg-soft)' }}>
                    <td style={{ ...td, fontWeight: 700 }}>Итого · {monthName} {curYear}</td>
                    <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: 'var(--green)' }}>+{fmtNum(tot.income)} ₽</td>
                    <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: 'var(--coral)' }}>−{fmtNum(tot.cost)} ₽</td>
                    <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: (tot.income - tot.cost) >= 0 ? 'var(--green)' : 'var(--coral)' }}>{(tot.income - tot.cost) >= 0 ? '+' : '−'}{fmtNum(Math.abs(tot.income - tot.cost))} ₽</td>
                  </tr>
                )}
              </tbody>
            </table>
            <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--text-dim)' }}>
              <Icon name="info" size={14} style={{ flex: 'none' }} />
              Доход — оплаченные занятия месяца. Расход — сколько за них получает преподаватель (ставка на дату занятия + надбавка; «факт» = уже выплачено). Выплаченная в этом месяце зарплата показана выше, в расходах школы, — она может относиться к другому периоду.
            </div>
          </Card>
        );
      })}
    </div>
  );
}

// ============================ REPORT TAB ============================
// ============================ ANALYTICS TAB ============================
const fmtK = (n) => { const a = Math.abs(n); return a >= 1000 ? (n / 1000).toFixed(a % 1000 ? 1 : 0) + 'k' : String(n); };

// Single-series bar chart with hover tooltip + grow-on-hover.
function BarChartCard({ title, unit, labels, values, color, accent }) {
  const [hi, setHi] = React.useState(null);
  const max = Math.max(...values, 1);
  const c = color || 'var(--brand)';
  return (
    <Card pad={24}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <Overline>{title}</Overline>
        <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-dim)' }}>{unit}</span>
      </div>
      <div style={{ position: 'relative', display: 'flex', alignItems: 'flex-end', gap: 6, height: 150, marginTop: 26 }}>
        {values.map((v, i) => (
          <div key={i} onMouseEnter={() => setHi(i)} onMouseLeave={() => setHi(null)}
            style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, cursor: 'pointer', position: 'relative' }}>
            {hi === i && (
              <div style={{ position: 'absolute', bottom: 'calc(100% - 18px)', left: '50%', transform: 'translateX(-50%)', background: 'var(--text)', color: '#fff', fontSize: 11.5, fontWeight: 600, fontFamily: 'var(--font-mono)', padding: '4px 8px', borderRadius: 6, whiteSpace: 'nowrap', zIndex: 5, boxShadow: 'var(--shadow-md)' }}>
                {new Intl.NumberFormat('ru-RU').format(v)} тыс. ₽
              </div>
            )}
            <div className="om-grow-up" style={{ width: '100%', height: Math.max((v / max) * 128, 2), borderRadius: '4px 4px 0 0', background: hi === i ? c : (accent ? c : 'color-mix(in oklab, ' + c + ' 28%, white)'), opacity: hi == null || hi === i ? 1 : 0.55, transition: 'background 140ms var(--ease), opacity 140ms var(--ease), filter 140ms var(--ease)', filter: hi === i ? 'brightness(0.94)' : 'none', animationDelay: i * 38 + 'ms' }} />
            <span style={{ fontSize: 10.5, color: hi === i ? 'var(--text)' : 'var(--text-dim)', fontWeight: 600, whiteSpace: 'nowrap' }}>{labels[i]}</span>
          </div>
        ))}
      </div>
    </Card>
  );
}

// Two-series grouped bars (доходы vs расходы) with hover.
function DualBarChart({ title, unit, labels, a, b, aLabel, bLabel }) {
  const [hi, setHi] = React.useState(null);
  const max = Math.max(...a, ...b, 1);
  return (
    <Card pad={24}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <Overline>{title}</Overline>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'var(--text-secondary)' }}><span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--green)' }} />{aLabel}</span>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'var(--text-secondary)' }}><span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--coral)' }} />{bLabel}</span>
        </div>
      </div>
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, height: 160, marginTop: 24 }}>
        {labels.map((lab, i) => (
          <div key={i} onMouseEnter={() => setHi(i)} onMouseLeave={() => setHi(null)} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, cursor: 'pointer', position: 'relative' }}>
            {hi === i && (
              <div style={{ position: 'absolute', bottom: 'calc(100% - 6px)', left: '50%', transform: 'translateX(-50%)', background: 'var(--text)', color: '#fff', fontSize: 11, fontWeight: 600, fontFamily: 'var(--font-mono)', padding: '5px 9px', borderRadius: 6, whiteSpace: 'nowrap', zIndex: 5, boxShadow: 'var(--shadow-md)', lineHeight: 1.5 }}>
                <div style={{ color: '#86efac' }}>+{new Intl.NumberFormat('ru-RU').format(a[i])} тыс. ₽</div>
                <div style={{ color: '#fca5a5' }}>−{new Intl.NumberFormat('ru-RU').format(b[i])} тыс. ₽</div>
              </div>
            )}
            <div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 134, width: '100%', justifyContent: 'center' }}>
              <div className="om-grow-up" style={{ width: '42%', height: Math.max((a[i] / max) * 134, 2), borderRadius: '3px 3px 0 0', background: 'var(--green)', opacity: hi == null || hi === i ? 1 : 0.5, transition: 'opacity 140ms var(--ease)', animationDelay: i * 38 + 'ms' }} />
              <div className="om-grow-up" style={{ width: '42%', height: Math.max((b[i] / max) * 134, 2), borderRadius: '3px 3px 0 0', background: 'var(--coral)', opacity: hi == null || hi === i ? 1 : 0.5, transition: 'opacity 140ms var(--ease)', animationDelay: (i * 38 + 60) + 'ms' }} />
            </div>
            <span style={{ fontSize: 10.5, color: hi === i ? 'var(--text)' : 'var(--text-dim)', fontWeight: 600 }}>{lab}</span>
          </div>
        ))}
      </div>
    </Card>
  );
}

// Horizontal category breakdown with hover.
function CategoryChart({ title, items }) {
  const [hi, setHi] = React.useState(null);
  const max = Math.max(...items.map(i => i.value), 1);
  const palette = ['var(--brand)', 'var(--coral)', 'var(--amber)', 'var(--green)', 'var(--gold)'];
  return (
    <Card pad={24}>
      <Overline>{title}</Overline>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 18 }}>
        {items.map((it, i) => (
          <div key={i} onMouseEnter={() => setHi(i)} onMouseLeave={() => setHi(null)} style={{ cursor: 'pointer' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, marginBottom: 5 }}>
              <span style={{ color: hi === i ? 'var(--text)' : 'var(--text-secondary)', fontWeight: hi === i ? 600 : 500 }}>{it.label}</span>
              <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--text)' }}>{new Intl.NumberFormat('ru-RU').format(it.value)} ₽</span>
            </div>
            <div style={{ height: 10, borderRadius: 5, background: 'var(--bg-soft)', overflow: 'hidden' }}>
              <div className="om-grow-right" style={{ width: (it.value / max) * 100 + '%', height: '100%', background: palette[i % palette.length], opacity: hi == null || hi === i ? 1 : 0.55, transition: 'opacity 140ms var(--ease), filter 140ms var(--ease)', filter: hi === i ? 'brightness(0.94)' : 'none', animationDelay: i * 45 + 'ms' }} />
            </div>
          </div>
        ))}
      </div>
    </Card>
  );
}

// Range calendar — popover with month grid, range selection, "Весь месяц" quick pick.
const WD = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
const MON = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
function RangeCalendar({ value, onApply, onClose, single, maxDate, maxISO }) {
  // Дефолт-месяц — ТЕКУЩИЙ (из APP_TODAY_ISO), а не захардкоженный май.
  const cap = maxISO || maxDate; // верхняя граница дат: дизайн зовёт maxISO, старый код — maxDate
  const _tISO = window.APP_TODAY_ISO || '2026-05-31';
  const init = value && value.start ? new Date(value.start) : new Date(+_tISO.slice(0, 4), +_tISO.slice(5, 7) - 1, 1);
  const [vy, setVy] = React.useState(init.getFullYear());
  const [vm, setVm] = React.useState(init.getMonth());
  const [start, setStart] = React.useState(value ? value.start : null);
  const [end, setEnd] = React.useState(value ? value.end : null);
  const iso = (y, m, d) => `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  const firstDow = (new Date(vy, vm, 1).getDay() + 6) % 7; // Mon=0
  const days = new Date(vy, vm + 1, 0).getDate();
  const pick = (d) => {
    const s = iso(vy, vm, d);
    if (single) { onApply({ start: s, end: s }); onClose(); return; }
    if (!start || (start && end)) { setStart(s); setEnd(null); }
    else { if (s < start) { setEnd(start); setStart(s); } else setEnd(s); }
  };
  const inRange = (d) => { const s = iso(vy, vm, d); return start && end && s >= start && s <= end; };
  const isEdge = (d) => { const s = iso(vy, vm, d); return s === start || s === end; };
  const clampEnd = (e) => (cap && e > cap ? cap : e);
  const wholeMonth = () => { setStart(iso(vy, vm, 1)); setEnd(clampEnd(iso(vy, vm, days))); };
  const quickMonth = (m) => { const dd = new Date(vy, m + 1, 0).getDate(); setVm(m); setStart(iso(vy, m, 1)); setEnd(clampEnd(iso(vy, m, dd))); setShowMonths(false); };
  const [showMonths, setShowMonths] = React.useState(false);
  const fmt = (s) => { if (!s) return '—'; const [y, m, d] = s.split('-'); return `${+d} ${MON[+m - 1].slice(0, 3).toLowerCase()}`; };
  const nav = (delta) => { let m = vm + delta, y = vy; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } setVm(m); setVy(y); };
  return (
    <div className="om-fade-in" style={{ position: 'absolute', top: 'calc(100% + 6px)', left: 0, zIndex: 9999, width: 300, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', padding: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
        <button onClick={() => nav(-1)} style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)' }}>‹</button>
        <button onClick={() => setShowMonths(v => !v)} style={{ fontSize: 14, fontWeight: 700, border: 'none', background: 'none', cursor: 'pointer', color: 'var(--text)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>{MON[vm]} {vy}<Icon name="chevron-down" size={14} style={{ color: 'var(--text-dim)' }} /></button>
        <button onClick={() => nav(1)} style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)' }}>›</button>
      </div>
      {single && <div style={{ fontSize: 11.5, color: 'var(--text-dim)', margin: '-2px 0 8px' }}>Выберите дату</div>}
      {showMonths && (
        <div className="om-fade-in" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, marginBottom: 10 }}>
          {MON.map((m, i) => (
            <button key={m} onClick={() => quickMonth(i)} style={{
              padding: '8px 4px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: i === vm ? 700 : 500,
              border: '1px solid ' + (i === vm ? 'var(--brand)' : 'var(--border)'), background: i === vm ? 'var(--brand-soft)' : 'var(--bg-card)', color: i === vm ? 'var(--brand-ink)' : 'var(--text-secondary)',
            }}>{m.slice(0, 3)}</button>
          ))}
        </div>
      )}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2, marginBottom: 4 }}>
        {WD.map(w => <div key={w} style={{ textAlign: 'center', fontSize: 10.5, fontWeight: 700, color: 'var(--text-dim)', padding: '2px 0' }}>{w}</div>)}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
        {Array.from({ length: firstDow }).map((_, i) => <div key={'e' + i} />)}
        {Array.from({ length: days }).map((_, i) => {
          const d = i + 1; const edge = isEdge(d); const mid = inRange(d) && !edge;
          const dis = cap && iso(vy, vm, d) > cap; // будущие даты недоступны (для выплат)
          return (
            <button key={d} disabled={dis} onClick={() => !dis && pick(d)} title={dis ? 'Будущая дата недоступна' : ''} style={{
              height: 32, border: 'none', cursor: dis ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: edge ? 700 : 500,
              borderRadius: edge ? 'var(--radius-sm)' : mid ? 0 : 'var(--radius-sm)',
              background: edge ? 'var(--brand)' : mid ? 'var(--brand-soft)' : 'transparent',
              color: dis ? 'var(--text-dim)' : edge ? '#fff' : mid ? 'var(--brand-ink)' : 'var(--text)', opacity: dis ? 0.4 : 1,
            }}>{d}</button>
          );
        })}
      </div>
      <button onClick={wholeMonth} style={{ width: '100%', marginTop: 10, padding: '7px', borderRadius: 'var(--radius-sm)', border: '1px dashed var(--border-strong)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--brand-ink)', fontSize: 12.5, fontWeight: 600, fontFamily: 'var(--font-sans)', display: single ? 'none' : 'block' }}>Весь месяц ({MON[vm]} 1–{days})</button>
      <div style={{ display: single ? 'none' : 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
        <span style={{ fontSize: 12.5, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>{fmt(start)} – {fmt(end)}</span>
        <Button variant="primary" size="sm" onClick={() => { if (start) { onApply({ start, end: end || start }); onClose(); } }} style={start ? {} : { opacity: 0.5, cursor: 'not-allowed' }}>Применить</Button>
      </div>
    </div>
  );
}

function ReportTab() {
  const { INVOICES, EXPENSES, PAYOUTS } = window.CRM_DATA;
  // Текущий месяц (0-based) из реальной «сегодня» — чтобы отчёт не «залипал» в мае.
  const CUR_M = (+((window.APP_TODAY_ISO || '2026-05-31').slice(5, 7))) - 1;
  const MONTH_SLUGS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
  const [period, setPeriod] = React.useState('year');   // week | month | year | range
  const [slice, setSlice] = React.useState(MONTH_SLUGS[CUR_M]); // month/range picker: по умолчанию текущий месяц
  const [range, setRange] = React.useState(null);        // {start, end} ISO
  const [calOpen, setCalOpen] = React.useState(false);

  // Mock series per period
  const SERIES = {
    week:  { labels: ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'], rev: [9, 12, 7, 14, 11, 5, 3], exp: [4, 3, 5, 6, 4, 2, 1] },
    month: { labels: ['Нед 1','Нед 2','Нед 3','Нед 4'], rev: [28, 34, 31, 41], exp: [16, 12, 18, 14] },
    year:  { labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], rev: [62, 48, 71, 58, 80, 66, 91, 74, 88, 79, 102, 124], exp: [40, 38, 45, 41, 52, 44, 58, 49, 55, 51, 60, 70] },
    range: { labels: ['1','5','10','15','20','25','30'], rev: [4, 9, 6, 12, 8, 14, 10], exp: [3, 4, 2, 6, 5, 7, 4] },
  };
  const s = SERIES[period] || SERIES.year;
  const profit = s.rev.map((r, i) => r - s.exp[i]);

  const income = INVOICES.filter(i => i.status === 'paid').reduce((a, b) => a + b.amount, 0);
  const refunds = INVOICES.filter(i => i.status === 'refund').reduce((a, b) => a + b.amount, 0);
  const expense = EXPENSES.reduce((a, b) => a + b.amount, 0) + PAYOUTS.filter(p => p.status === 'paid' && !p.voidedAt).reduce((a, b) => a + b.amount, 0);
  const net = income - refunds - expense;

  // Expense categories breakdown
  const catMap = {};
  EXPENSES.forEach(e => { catMap[e.category] = (catMap[e.category] || 0) + e.amount; });
  PAYOUTS.filter(p => p.status === 'paid' && !p.voidedAt).forEach(p => { catMap['Зарплаты'] = (catMap['Зарплаты'] || 0) + p.amount; });
  const catItems = Object.entries(catMap).map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value);

  const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
  const sliceOpts = period === 'year'
    ? [{ value: '2026', label: '2026' }, { value: '2025', label: '2025' }]
    : period === 'month'
      ? MONTHS.map((m, i) => ({ value: ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'][i], label: m + ' 2026' }))
      : [{ value: 'thisweek', label: 'Эта неделя' }, { value: 'lastweek', label: 'Прошлая неделя' }];
  const sliceVal = sliceOpts.find(o => o.value === slice) ? slice : sliceOpts[0].value;

  const Row = ({ label, value, color }) => (
    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '11px 0', borderBottom: '1px solid var(--border)' }}>
      <span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-secondary)' }}>{label}</span>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 600, color }}>{new Intl.NumberFormat('ru-RU').format(value)} ₽</span>
    </div>
  );

  const PERIODS = [{ id: 'week', label: 'Неделя' }, { id: 'month', label: 'Месяц' }, { id: 'year', label: 'Год' }, { id: 'range', label: 'Промежуток' }];
  const rangeLabel = range ? (() => { const f = (s) => { const [y, m, d] = s.split('-'); return `${+d} ${MON[+m - 1].slice(0, 3).toLowerCase()}`; }; return f(range.start) + ' – ' + f(range.end); })() : 'Выбрать даты';

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
      {/* Period controls */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
        <div style={{ display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 'var(--radius-sm)', padding: 3, gap: 2 }}>
          {PERIODS.map(p => (
            <button key={p.id} onClick={() => setPeriod(p.id)} style={{
              padding: '6px 16px', border: 'none', borderRadius: 5, cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600,
              background: period === p.id ? 'var(--bg-card)' : 'transparent', color: period === p.id ? 'var(--brand-ink)' : 'var(--text-dim)', boxShadow: period === p.id ? 'var(--shadow-xs)' : 'none',
              transition: 'background 160ms var(--ease), color 160ms var(--ease)',
            }}>{p.label}</button>
          ))}
        </div>
        {period === 'range' ? (
          <div style={{ position: 'relative' }}>
            <button onClick={() => setCalOpen(o => !o)} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 12px', cursor: 'pointer', background: 'var(--bg-card)', border: '1px solid ' + (calOpen ? 'var(--brand)' : 'var(--border)'), borderRadius: 'var(--radius-sm)', boxShadow: calOpen ? 'var(--shadow-focus)' : 'none', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 500, color: range ? 'var(--text)' : 'var(--text-secondary)' }}>
              <Icon name="calendar" size={15} style={{ color: 'var(--text-dim)' }} />{rangeLabel}
            </button>
            {calOpen && <RangeCalendar value={range} onApply={setRange} onClose={() => setCalOpen(false)} />}
          </div>
        ) : (
          <Dropdown value={sliceVal} onChange={setSlice} options={sliceOpts} width={170} searchable={period === 'month'} />
        )}
        <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Динамика {period === 'year' ? 'по месяцам' : period === 'range' ? 'по дням промежутка' : period === 'month' ? 'по неделям' : 'по дням'}</span>
      </div>

      {/* Summary KPIs */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 20 }}>
        <KpiTile overline="Доход" value={fmtRub(income)} sub="оплаченные счета" accent="green" />
        <KpiTile overline="Расход" value={fmtRub(expense)} sub="школа + зарплаты" accent="coral" />
        <KpiTile overline="Возвраты" value={fmtRub(refunds)} sub={INVOICES.filter(i=>i.status==='refund').length + ' операц.'} accent="amber" />
        <KpiTile overline="Прибыль" value={fmtRub(net)} sub="за период" accent={net >= 0 ? 'green' : 'coral'} />
      </div>

      {/* Charts grid */}
      <div key={period} className="om-fade-in" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
        <BarChartCard title="Выручка" unit="тыс. ₽" labels={s.labels} values={s.rev} color="var(--brand)" />
        <DualBarChart title="Доходы и расходы" labels={s.labels} a={s.rev} b={s.exp} aLabel="Доход" bLabel="Расход" />
        <BarChartCard title="Прибыль" unit="тыс. ₽" labels={s.labels} values={profit.map(p => Math.max(p, 0))} color="var(--green)" accent />
        <CategoryChart title="Расходы по категориям" items={catItems} />
      </div>

      {/* Net summary */}
      <Card pad={24} style={{ maxWidth: 460 }}>
        <Overline>Итог за {MONTHS[CUR_M].toLowerCase()}</Overline>
        <div style={{ marginTop: 10 }}>
          <Row label="Доход (оплаченные счета)" value={income} color="var(--green)" />
          <Row label="Возвраты" value={-refunds} color="var(--coral)" />
          <Row label="Расходы школы + зарплаты" value={-expense} color="var(--coral)" />
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', paddingTop: 13 }}>
            <span style={{ fontSize: 15, fontWeight: 700 }}>Прибыль</span>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 700, color: net >= 0 ? 'var(--green)' : 'var(--coral)' }}>{new Intl.NumberFormat('ru-RU').format(net)} ₽</span>
          </div>
        </div>
      </Card>
    </div>
  );
}

// ============================ SHELL ============================
function Finances({ onAddInvoice, onToast, onUpdateClientBalance, onConfirm, onAddExpense, onNavSettings, onPayout }) {
  const [tab, setTab] = React.useState('invoices');
  const FIN_MON = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
  const finTodayISO = window.APP_TODAY_ISO || '2026-05-31';
  const finCurYear = +finTodayISO.slice(0, 4);
  const [vMonth, setVMonth] = React.useState(+finTodayISO.slice(5, 7) - 1);
  const TABS = [{ id: 'invoices', label: 'Выставление счетов' }, { id: 'expenses', label: 'Расходы и доходы' }, { id: 'report', label: 'Аналитика' }];

  return (
    <div style={{ padding: '24px 32px 40px' }}>
      {/* Sub-tabs */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 20 }}>
      <div style={{ display: 'inline-flex', background: 'var(--bg-soft)', borderRadius: 'var(--radius-sm)', padding: 3, gap: 2 }}>
        {TABS.map(t => (
          <button key={t.id} onClick={() => setTab(t.id)}
            onMouseEnter={e => { if (tab !== t.id) { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-secondary)'; } }}
            onMouseLeave={e => { if (tab !== t.id) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-dim)'; } }}
            style={{
            padding: '7px 18px', border: 'none', borderRadius: 5, cursor: 'pointer',
            fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600,
            background: tab === t.id ? 'var(--bg-card)' : 'transparent',
            color: tab === t.id ? 'var(--brand-ink)' : 'var(--text-dim)',
            boxShadow: tab === t.id ? 'var(--shadow-xs)' : 'none',
            transition: 'background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease)',
          }}>{t.label}</button>
        ))}
      </div>
      {tab === 'expenses' && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
          <button onClick={() => setVMonth(m => (m + 11) % 12)} title="Предыдущий месяц" style={{ width: 32, height: 32, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}><Icon name="chevron-left" size={16} /></button>
          <Dropdown value={vMonth} onChange={setVMonth} options={FIN_MON.map((m, i) => ({ value: i, label: m + ' ' + finCurYear }))} width={150} searchable />
          <button onClick={() => setVMonth(m => (m + 1) % 12)} title="Следующий месяц" style={{ width: 32, height: 32, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}><Icon name="chevron-right" size={16} /></button>
        </div>
      )}
      </div>

      <div key={tab} className="om-fade-in">
        {tab === 'invoices' && <InvoicesTab onAddInvoice={onAddInvoice} onToast={onToast} onUpdateClientBalance={onUpdateClientBalance} onConfirm={onConfirm} />}
        {tab === 'expenses' && <ExpensesTab onToast={onToast} onAddExpense={onAddExpense} onNavSettings={onNavSettings} onPayout={onPayout} vMonth={vMonth} setVMonth={setVMonth} />}
        {tab === 'report' && <ReportTab />}
      </div>
    </div>
  );
}

Object.assign(window, { Finances, RangeCalendar });
