/* global React, Icon, Avatar, Pill, Button, Overline, Card, Dropdown, Balance, RangeCalendar, TeacherCalendar */
// Schools.ArtemenkoCRM — Карточка сотрудника (полноэкранная страница)

const SC_ROLE = {
  teacher: { label: 'Преподаватель', bg: 'var(--brand-soft)', fg: 'var(--brand-ink)' },
  manager: { label: 'Менеджер', bg: 'var(--green-bg)', fg: 'var(--green-ink)' },
  admin:   { label: 'Администратор', bg: 'var(--gold-soft)', fg: 'var(--gold-ink)' },
};
const SC_STATUS = {
  active:   { label: 'Работает', dot: 'var(--green)', fg: 'var(--green-ink)' },
  vacation: { label: 'Отпуск', dot: 'var(--amber)', fg: 'var(--amber-ink)' },
  fired:    { label: 'Уволен', dot: 'var(--text-dim)', fg: 'var(--text-dim)' },
};
const SC_LESSON = (window.CRM_DATA && window.CRM_DATA.LESSON_STATUS) || {};
// В карточке ПРЕПОДА (money-free) статус paid/debt = проведённое занятие → показываем «Проведено»,
// а не «Оплачено» (оплата — забота менеджера, не преподавателя).
const scStatus = (status) => { const s = SC_LESSON[status] || {}; return (status === 'paid' || status === 'debt') ? { ...s, label: 'Проведено' } : s; };
const SC_WDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
// Нормализует время к «HH:MM»: «9:00»→«09:00», «9»→«09:00», «9:5»→«09:05». Пустое/мусор → как есть.
const scNormTime = (t) => { const m = String(t == null ? '' : t).trim().match(/^(\d{1,2})(?::(\d{1,2}))?$/); return m ? String(+m[1]).padStart(2, '0') + ':' + String(+(m[2] || 0)).padStart(2, '0') : String(t == null ? '' : t); };
const SC_MONTHS = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'];
const SC_MONTHS_FULL = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
const SC_TODAY = new Date(2026, 4, 31);
const fmtNum = (n) => new Intl.NumberFormat('ru-RU').format(n);

function SCRow({ label, value, mono, onCopy }) {
  if (!value) return null;
  if (onCopy) {
    return (
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '9px 0', borderBottom: '1px solid var(--border)' }}>
        <span style={{ fontSize: 13, color: 'var(--text-dim)', flex: 'none' }}>{label}</span>
        <button type="button" onClick={() => onCopy(value)} title="Нажмите, чтобы скопировать"
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; const i = e.currentTarget.querySelector('svg'); if (i) i.style.opacity = 1; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; const i = e.currentTarget.querySelector('svg'); if (i) i.style.opacity = 0; }}
          style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: 'none', background: 'transparent', cursor: 'pointer', padding: '2px 6px', margin: '-2px -6px', borderRadius: 'var(--radius-sm)', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 14, fontWeight: 600, color: 'var(--text)', textAlign: 'right', transition: 'background var(--dur-fast) var(--ease)' }}>
          <Icon name="copy" size={13} style={{ color: 'var(--text-dim)', opacity: 0, flex: 'none', transition: 'opacity var(--dur-fast) var(--ease)' }} />{value}
        </button>
      </div>
    );
  }
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '9px 0', borderBottom: '1px solid var(--border)' }}>
      <span style={{ fontSize: 13, color: 'var(--text-dim)' }}>{label}</span>
      <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', textAlign: 'right' }}>{value}</span>
    </div>
  );
}

// Masked credential row: hidden by default, click the eye to reveal, pencil to edit inline.
function SecretRow({ label, value, placeholder, onSave, error }) {
  const [revealed, setRevealed] = React.useState(false);
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(value || '');
  React.useEffect(() => { setDraft(value || ''); }, [value]);
  const has = !!(value && value.length);
  const showErr = error && !has && !editing;
  const commit = () => { onSave && onSave(draft.trim()); setEditing(false); };
  const cancel = () => { setDraft(value || ''); setEditing(false); };
  const iconBtn = (active) => ({ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--border)', background: active ? 'var(--bg-soft)' : 'var(--bg-card)', cursor: 'pointer', color: active ? 'var(--brand-ink)' : 'var(--text-dim)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none', transition: 'background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast)' });

  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, padding: '9px 0', borderBottom: '1px solid var(--border)', minHeight: 38 }}>
      <span style={{ fontSize: 13, color: showErr ? 'var(--coral)' : 'var(--text-dim)', flex: 'none', fontWeight: showErr ? 600 : 400 }}>{label}{showErr ? ' *' : ''}</span>
      {editing ? (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1, justifyContent: 'flex-end' }}>
          <input autoFocus value={draft} onChange={e => setDraft(e.target.value)} placeholder={placeholder}
            onKeyDown={e => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') cancel(); }}
            style={{ flex: 1, minWidth: 0, maxWidth: 150, border: '1px solid var(--brand)', boxShadow: 'var(--shadow-focus)', borderRadius: 6, padding: '5px 9px', fontFamily: 'var(--font-mono)', fontSize: 13.5, color: 'var(--text)', outline: 'none', background: 'var(--bg-card)', textAlign: 'right' }} />
          <button onClick={commit} title="Сохранить" style={iconBtn(true)}
            onMouseEnter={e => { e.currentTarget.style.background = 'var(--green-bg)'; e.currentTarget.style.color = 'var(--green-ink)'; e.currentTarget.style.borderColor = '#BFE0CC'; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.color = 'var(--brand-ink)'; e.currentTarget.style.borderColor = 'var(--border)'; }}>
            <Icon name="check" size={15} />
          </button>
          <button onClick={cancel} title="Отмена" style={iconBtn(false)}
            onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6"/></svg>
          </button>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
          <button onClick={() => has && setRevealed(r => !r)} title={has ? (revealed ? 'Скрыть' : 'Показать') : ''} disabled={!has}
            style={{ border: 'none', background: 'none', cursor: has ? 'pointer' : 'default', padding: 0, display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: has ? 'var(--text)' : showErr ? 'var(--coral)' : 'var(--text-dim)', letterSpacing: revealed || !has ? 'normal' : '0.12em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 160 }}>
              {has ? (revealed ? value : '•'.repeat(Math.min(value.length, 10))) : showErr ? 'не задан — укажите' : 'не задан'}
            </span>
            {has && <Icon name={revealed ? 'eye-off' : 'eye'} size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />}
          </button>
          <button onClick={() => { setDraft(value || ''); setEditing(true); }} title="Изменить" style={iconBtn(false)}
            onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.color = 'var(--brand-ink)'; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-dim)'; }}>
            <Icon name="pencil" size={13} />
          </button>
        </div>
      )}
    </div>
  );
}

function SCStat({ label, value, color, sub }) {
  return (
    <div style={{ minWidth: 0 }}>
      <div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700, whiteSpace: 'nowrap' }}>{label}</div>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700, marginTop: 4, color: color || 'var(--text)', whiteSpace: 'nowrap' }}>{value}</div>
      {sub && <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginTop: 2 }}>{sub}</div>}
    </div>
  );
}

// Small bar chart with hover.
// scroll: колонок больше, чем влезает (напр. 24 часа) → полоса прокручивается по горизонтали,
// каждая колонка фиксированной ширины (minW), и при смене данных вид подъезжает к первой ненулевой.
function MiniBars({ labels, values, unit, height = 120, scroll = false, minW = 34 }) {
  const [hi, setHi] = React.useState(null);
  const max = Math.max(...values, 1);
  const boxRef = React.useRef(null);
  React.useEffect(() => {
    if (!scroll || !boxRef.current) return;
    const first = values.findIndex(v => v > 0);
    // ставим первый занятый час чуть левее края — видно и его, и соседей.
    // Через rAF: до отрисовки ширина полосы ещё нулевая и scrollLeft клампится в 0.
    const to = first > 1 ? Math.max(0, (first - 1) * (minW + 5)) : 0;
    const id = requestAnimationFrame(() => { if (boxRef.current) boxRef.current.scrollLeft = to; });
    return () => cancelAnimationFrame(id);
  }, [scroll, minW, labels.join(','), values.join(',')]);
  return (
    <div ref={boxRef} style={scroll ? { overflowX: 'auto', overflowY: 'hidden', paddingBottom: 4 } : undefined}>
    <div style={{ display: 'flex', alignItems: 'flex-end', gap: 5, height: height + 16, marginTop: 16, minWidth: scroll ? values.length * (minW + 5) : undefined }}>
      {values.map((v, i) => (
        <div key={i} onMouseEnter={() => setHi(i)} onMouseLeave={() => setHi(null)} style={{ flex: scroll ? '0 0 ' + minW + 'px' : 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, cursor: 'pointer', position: 'relative' }}>
          {hi === i && <div style={{ position: 'absolute', bottom: 'calc(100% - 12px)', left: '50%', transform: 'translateX(-50%)', background: 'var(--text)', color: '#fff', fontSize: 11, fontWeight: 600, fontFamily: 'var(--font-mono)', padding: '3px 7px', borderRadius: 6, whiteSpace: 'nowrap', zIndex: 5 }}>{v} {unit}</div>}
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, lineHeight: 1, color: v === 0 ? 'var(--text-dim)' : hi === i ? 'var(--brand-ink)' : 'var(--text-secondary)', opacity: v === 0 ? 0.5 : 1 }}>{v}</span>
          <div className="om-grow-up" style={{ width: '100%', height: Math.max((v / max) * (height - 28), 2), borderRadius: '4px 4px 0 0', background: hi === i ? 'var(--brand)' : 'var(--brand-soft)', transition: 'background 140ms var(--ease)', animationDelay: i * 32 + 'ms' }} />
          <span style={{ fontSize: 10.5, color: hi === i ? 'var(--text)' : 'var(--text-dim)', fontWeight: 600 }}>{labels[i]}</span>
        </div>
      ))}
    </div>
    </div>
  );
}

function daysSince(iso) {
  if (!iso) return null;
  const d = new Date(iso + 'T00:00:00');
  return Math.round((SC_TODAY - d) / 86400000);
}
function lastLessonLabel(iso) {
  const n = daysSince(iso);
  if (n == null) return 'нет занятий';
  if (n <= 0) return 'сегодня';
  if (n === 1) return 'вчера';
  return `${n} дн. назад`;
}

// Multi-select student picker modal — search + multi-status filter + select/deselect
// visible. Scrolling the list never closes it (only backdrop / Отмена / ✕ / Esc).
const SP_STATUS_KEYS = ['new', 'call', 'trial', 'payment', 'active', 'refusal'];
function StudentPickerModal({ candidates, onAssign, onClose }) {
  const [vis, setVis] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [statuses, setStatuses] = React.useState([]);
  const [sel, setSel] = React.useState([]);
  const PILL = window.PILL || {};
  React.useEffect(() => { const t = setTimeout(() => setVis(true), 10); return () => clearTimeout(t); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') close(); };
    document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey);
  }, []);
  const close = () => { setVis(false); setTimeout(onClose, 190); };

  const filtered = candidates.filter(c => {
    if (q && !`${c.child} ${c.teacher || ''}`.toLowerCase().includes(q.toLowerCase())) return false;
    if (statuses.length && !statuses.includes(c.status)) return false;
    return true;
  });
  const visibleIds = filtered.map(c => c.id);
  const toggle = (id) => setSel(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);
  const toggleStatus = (k) => setStatuses(s => s.includes(k) ? s.filter(x => x !== k) : [...s, k]);
  const selectVisible = () => setSel(s => Array.from(new Set([...s, ...visibleIds])));
  const deselectVisible = () => setSel(s => s.filter(id => !visibleIds.includes(id)));
  const visibleSelectedCount = visibleIds.filter(id => sel.includes(id)).length;
  const allVisibleSelected = visibleIds.length > 0 && visibleSelectedCount === visibleIds.length;

  const chipBtn = (active, color) => ({
    display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 11px', cursor: 'pointer',
    borderRadius: 'var(--radius-pill)', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600,
    border: '1px solid ' + (active ? color : 'var(--border)'), background: active ? 'var(--bg-soft)' : 'var(--bg-card)',
    color: active ? 'var(--text)' : 'var(--text-secondary)', boxShadow: active ? 'inset 0 0 0 1px ' + color : 'none',
    transition: 'border-color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-fast)',
  });
  const smallBtn = (disabled) => ({
    display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 11px', borderRadius: 'var(--radius-sm)',
    border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: disabled ? 'default' : 'pointer',
    fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, color: disabled ? 'var(--text-dim)' : 'var(--text-secondary)',
    opacity: disabled ? 0.55 : 1, transition: 'background var(--dur-fast), border-color var(--dur-fast)',
  });

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 70, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div onClick={close} style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.45)', backdropFilter: 'blur(2px)', opacity: vis ? 1 : 0, transition: 'opacity 190ms var(--ease)' }} />
      <div style={{ position: 'relative', width: 540, maxWidth: '100%', height: 'min(640px, 88vh)', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', display: 'flex', flexDirection: 'column', overflow: 'hidden', opacity: vis ? 1 : 0, transform: vis ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.97)', transition: 'opacity 190ms var(--ease), transform 220ms cubic-bezier(0.34,1.4,0.5,1)' }}>
        {/* Header */}
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <h3 style={{ fontSize: 17, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Добавить учеников</h3>
            <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 2 }}>Закрепите одного или нескольких учеников из базы</div>
          </div>
          <button onClick={close} title="Закрыть" onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}
            style={{ width: 34, height: 34, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none', transition: 'background var(--dur-fast)' }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"><path d="M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6"/></svg>
          </button>
        </div>

        {/* Controls (fixed, do not scroll) */}
        <div style={{ padding: '14px 20px 12px', borderBottom: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', padding: '9px 12px' }}>
            <Icon name="search" size={16} style={{ color: 'var(--text-dim)' }} />
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Поиск ученика…" style={{ border: 'none', outline: 'none', background: 'transparent', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text)' }} />
            {q && <button onClick={() => setQ('')} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--text-dim)', display: 'inline-flex', padding: 0 }}><Icon name="x" size={15} /></button>}
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
            {SP_STATUS_KEYS.map(k => {
              const p = PILL[k] || { label: k, dot: 'var(--text-dim)' };
              const on = statuses.includes(k);
              return (
                <button key={k} onClick={() => toggleStatus(k)} style={chipBtn(on, p.dot)}
                  onMouseEnter={e => { if (!on) e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { if (!on) e.currentTarget.style.background = 'var(--bg-card)'; }}>
                  <span style={{ width: 7, height: 7, borderRadius: '50%', background: p.dot }} />{p.label}
                </button>
              );
            })}
            {statuses.length > 0 && <button onClick={() => setStatuses([])} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', fontSize: 12.5, fontWeight: 600, fontFamily: 'var(--font-sans)' }}>сбросить</button>}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <button onClick={selectVisible} disabled={allVisibleSelected || visibleIds.length === 0} style={smallBtn(allVisibleSelected || visibleIds.length === 0)}
              onMouseEnter={e => { if (!(allVisibleSelected || visibleIds.length === 0)) { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; } }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; }}>
              <Icon name="check" size={14} />Выбрать видимых ({visibleIds.length})
            </button>
            <button onClick={deselectVisible} disabled={visibleSelectedCount === 0} style={smallBtn(visibleSelectedCount === 0)}
              onMouseEnter={e => { if (visibleSelectedCount !== 0) { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; } }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; }}>
              Снять выбор с видимых
            </button>
            <div style={{ flex: 1 }} />
            <span style={{ fontSize: 12.5, color: 'var(--text-dim)', fontFamily: 'var(--font-mono)' }}>выбрано {sel.length}</span>
          </div>
        </div>

        {/* Scrollable list */}
        <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '8px 12px', minHeight: 120 }}>
          {filtered.length === 0 ? (
            <div style={{ padding: '32px 16px', textAlign: 'center', color: 'var(--text-dim)', fontSize: 14 }}>По заданным условиям учеников нет</div>
          ) : filtered.map(c => {
            const on = sel.includes(c.id);
            return (
              <div key={c.id} onClick={() => toggle(c.id)} className="row-hover" style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '9px 10px', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}>
                <span style={{ width: 20, height: 20, borderRadius: 5, border: '1.5px solid ' + (on ? 'var(--brand)' : 'var(--border-strong)'), background: on ? 'var(--brand)' : 'var(--bg-card)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none', transition: 'background var(--dur-fast), border-color var(--dur-fast)' }}>
                  {on && <Icon name="check" size={13} strokeWidth={3} style={{ color: '#fff' }} />}
                </span>
                <Avatar animal={c.animal} tone={c.tone} size={32} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.child}</div>
                  <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{c.teacher ? 'сейчас: ' + c.teacher.split(' ').slice(0, 2).join(' ') : 'без преподавателя'}</div>
                </div>
                <Pill kind={c.status} />
                <Balance value={c.balance || 0} />
              </div>
            );
          })}
        </div>

        {/* Footer */}
        <div style={{ padding: '14px 20px', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ flex: 1 }} />
          <Button variant="ghost" onClick={close}>Отмена</Button>
          <Button variant="primary" icon="add-student" onClick={() => sel.length && onAssign(sel)} style={sel.length ? undefined : { opacity: 0.5, cursor: 'default', boxShadow: 'none' }}>
            {sel.length ? `Добавить (${sel.length})` : 'Добавить'}
          </Button>
        </div>
      </div>
    </div>
  );
}

function StaffPage({ staff, onClose, onEdit, onPayout, onOpenClient, onToast, onOpenCabinet, payoutVer }) {
  const [tab, setTab] = React.useState('overview');  // overview | lessons | students | payouts
  const [notes, setNotes] = React.useState(staff && staff.notes ? staff.notes : '');
  const [notesEdit, setNotesEdit] = React.useState(false);
  const [login, setLogin] = React.useState(staff && staff.login ? staff.login : '');
  const [password, setPassword] = React.useState(staff && staff.password ? staff.password : '');
  const [accessError, setAccessError] = React.useState(false); // показывать ли красную обводку «Доступ в кабинет»
  const [accessShake, setAccessShake] = React.useState(0);     // bump → перезапуск тряски
  const accessRef = React.useRef(null);
  const scrollAreaRef = React.useRef(null);
  // Как только логин И пароль заданы — убираем красное.
  React.useEffect(() => { if (login.trim() && password.trim()) setAccessError(false); }, [login, password]);
  const [, force] = React.useReducer(x => x + 1, 0);
  const toast = onToast || (() => {});
  // Копирование любого контакта с оповещением.
  const copyVal = (v) => { if (navigator.clipboard) navigator.clipboard.writeText(v).catch(() => {}); toast('Скопировано: ' + v); };
  // Выплатить запланированную выплату прямо из истории (помечаем оплаченной «сегодня» по миру приложения).
  const fmtPayDate = (iso) => { const a = (iso || '').split('-'); return a.length === 3 ? `${+a[2]} ${['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'][+a[1] - 1]} ${a[0]}` : iso; };
  const [payConfirm, setPayConfirm] = React.useState(null); // запланированная выплата → модалка оплаты
  const [voidTarget, setVoidTarget] = React.useState(null); // выплата → подтверждение сторно (отмены)
  // Оплатить запланированную выплату: planned→paid на СЕРВЕРЕ (PATCH /payouts/:id/pay). Витрину мутируем
  // ТОЛЬКО после успеха (без тихого оптимизма); ошибка → тост + throw (модалка снимет busy).
  const doPayNow = async (h, opts) => {
    try {
      const body = {}; if (opts && opts.method) body.method = opts.method; if (opts && opts.payDate) body.payDate = opts.payDate;
      const res = await window.apiCall('/payouts/' + h.id + '/pay', { method: 'PATCH', body: JSON.stringify(body) });
      h.status = 'paid';
      h.payDate = fmtPayDate((res && res.payDate) || (opts && opts.payDate) || window.APP_TODAY_ISO || '');
      if (opts && opts.method) h.method = opts.method;
      if (!h.method || h.method === '—') h.method = (window.CRM_DATA.PAYMENT_METHODS || [])[0] || 'СБП';
      const fin = (window.CRM_DATA.PAYOUTS || []).find(p => p.id === h.id);
      if (fin) { fin.status = 'paid'; fin.date = h.payDate; fin.method = h.method; }
      setPayConfirm(null); toast('Выплачено: ' + fmtNum(h.amount) + ' ₽'); force();
    } catch (e) { toast('Не удалось оплатить: ' + (e.message || 'ошибка')); throw e; }
  };
  // Сторно (admin): DELETE /payouts/:id → voided_at. Занятия распускаются (снова к оплате). Мутируем после успеха.
  const doVoid = async (h) => {
    try {
      await window.apiCall('/payouts/' + h.id, { method: 'DELETE' });
      const at = new Date().toISOString();
      h.voidedAt = at;
      const fin = (window.CRM_DATA.PAYOUTS || []).find(p => p.id === h.id); if (fin) fin.voidedAt = at;
      setVoidTarget(null); toast('Выплата отменена · занятия вернулись в расчёт'); force();
      setReloadKey(v => v + 1); // снимок распущен → перечитать серверное превью (занятия снова к оплате)
    } catch (e) { toast('Не удалось отменить выплату: ' + (e.message || 'ошибка')); }
  };

  // Доступ в кабинет: логин/пароль СОХРАНЯЮТСЯ НА СЕРВЕРЕ (PATCH /staff/:id), а не «на экране».
  // Раньше SecretRow только мутировал витрину и показывал тост — учётка не менялась вовсе.
  // Смена пароля завершает прежние сессии сотрудника (это делает бэкенд).
  const saveAccess = async (patch) => {
    const isPwd = patch.password !== undefined;
    const val = (isPwd ? patch.password : patch.login) || '';
    if (!String(val).trim()) { toast(isPwd ? 'Введите новый пароль' : 'Введите логин'); return; }
    try {
      await window.apiCall('/staff/' + staff.id, { method: 'PATCH', body: JSON.stringify({ role: staff.role, ...patch }) });
      if (isPwd) { setPassword(val); staff.password = val; toast('Пароль обновлён · прежние входы завершены'); }
      else { setLogin(val); staff.login = val; toast('Логин обновлён'); }
      force();
    } catch (e) {
      toast('Не удалось сохранить: ' + (e.message || 'ошибка'));
    }
  };

  // Overview filters
  const [yearView, setYearView] = React.useState(2026);
  const [monthView, setMonthView] = React.useState(null);   // null = весь год, 0..11
  const [ovStatus, setOvStatus] = React.useState(null);
  const [ovSlide, setOvSlide] = React.useState(0);          // направление анимации при листании: +1 вперёд(›), −1 назад(‹), 0 — фейд

  // Текущий месяц из реальной «сегодня» — раздел «Занятия» больше не залипает в мае.
  const scToday = window.APP_TODAY_ISO || '2026-05-31';
  const scMonStart = scToday.slice(0, 7) + '-01';
  const scMonEnd = scToday.slice(0, 7) + '-' + String(new Date(+scToday.slice(0, 4), +scToday.slice(5, 7), 0).getDate()).padStart(2, '0');
  // Границы текущей НЕДЕЛИ (пн–вс) для блока «Занятия по статусам · за неделю».
  const scWeekStart = (() => { const d = new Date(+scToday.slice(0, 4), +scToday.slice(5, 7) - 1, +scToday.slice(8, 10)); d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; })();
  const scWeekEnd = (() => { const d = new Date(+scWeekStart.slice(0, 4), +scWeekStart.slice(5, 7) - 1, +scWeekStart.slice(8, 10)); d.setDate(d.getDate() + 6); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; })();
  // Lessons filters
  const [lessonStatus, setLessonStatus] = React.useState([]); // мультивыбор статусов проведения
  const [lessonStudent, setLessonStudent] = React.useState([]); // мультивыбор учеников
  const [range, setRange] = React.useState({ start: scMonStart, end: scMonEnd });
  const [calOpen, setCalOpen] = React.useState(false);
  const [dateSlide, setDateSlide] = React.useState(0); // направление анимации даты: +1 вперёд(›), −1 назад(‹), 0 без
  // Листание периода на предыдущий/следующий календарный месяц (по месяцу range.start).
  // Функциональная форма setState — устойчиво к быстрым повторным кликам (без устаревшего range).
  const shiftMonth = (delta) => {
    setDateSlide(delta);
    setRange((prev) => {
      const first = new Date(+prev.start.slice(0, 4), +prev.start.slice(5, 7) - 1 + delta, 1);
      const ny = first.getFullYear(), nm = first.getMonth() + 1, pad = (v) => String(v).padStart(2, '0');
      return { start: `${ny}-${pad(nm)}-01`, end: `${ny}-${pad(nm)}-${pad(new Date(ny, nm, 0).getDate())}` };
    });
  };
  const [newLesson, setNewLesson] = React.useState(null); // черновик нового занятия (для педагога)
  const [newLessonCal, setNewLessonCal] = React.useState(false); // открыт ли стилизованный календарь даты
  const [card, setCard] = React.useState(null); // «Карточка занятия» (LessonCard): {lesson} открыта (null → создание); null закрыта

  // Реальные занятия преподавателя за выбранный период — из API (тот же источник, что
  // «Личный календарь» ниже). Таблица «История занятий» показывает их, а не пустой client.__lessons.
  const [apiLessons, setApiLessons] = React.useState([]);
  const [histVer, setHistVer] = React.useState(0); // растёт при каждой загрузке → ремоунт tbody → анимация строк заново
  const [reloadKey, setReloadKey] = React.useState(0); // бамп → перечитать занятия (после создания/правки)
  React.useEffect(() => {
    if (staff.role !== 'teacher' || staff.id == null || !window.apiCall) { setApiLessons([]); return; }
    let alive = true;
    window.apiCall('/teachers/' + staff.id + '/lessons?from=' + range.start + '&to=' + range.end)
      .then((r) => { if (alive) { setApiLessons(r.lessons || []); setHistVer(v => v + 1); } })
      .catch(() => { if (alive) setApiLessons([]); });
    return () => { alive = false; };
  }, [staff.id, staff.role, range.start, range.end, reloadKey]);

  // Обзор: реальные занятия препода за выбранный ГОД (yearView) — money-free источник для
  // «Нагрузки и статистики», «Помесячной работы», «Занятий по статусам». Раньше обзор считал из
  // пустого client.__lessons → всё было по нулям.
  const [overviewLessons, setOverviewLessons] = React.useState([]);
  React.useEffect(() => {
    if (staff.role !== 'teacher' || staff.id == null || !window.apiCall) { setOverviewLessons([]); return; }
    let alive = true;
    window.apiCall('/teachers/' + staff.id + '/lessons?from=' + yearView + '-01-01&to=' + yearView + '-12-31')
      .then((r) => { if (alive) setOverviewLessons(r.lessons || []); })
      .catch(() => { if (alive) setOverviewLessons([]); });
    return () => { alive = false; };
  }, [staff.id, staff.role, yearView, reloadKey]);

  // Конфликты выплат — по РЕАЛЬНОМУ снимку (payout_lessons), а не по эвристике FIFO. Для каждого месяца
  // года с активной выплатой тянем серверное превью: toPay = реально НЕоплаченные занятия (NOT EXISTS по
  // активным payout_lessons). Так занятия, за которые уже уплачено, не попадают ошибочно в «к доплате».
  const [conflictPv, setConflictPv] = React.useState({}); // { mi: результат fetchPayoutPreview }
  React.useEffect(() => {
    if (staff.role !== 'teacher' || staff.id == null || !window.fetchPayoutPreview) { setConflictPv({}); return; }
    const hist = (window.CRM_DATA.PAYOUT_HISTORY || []).filter(p => p.teacherId === staff.id && !p.voidedAt && p.periodStart && +String(p.periodStart).slice(0, 4) === yearView);
    const months = Array.from(new Set(hist.map(p => +String(p.periodStart).slice(5, 7) - 1)));
    if (!months.length) { setConflictPv({}); return; }
    let alive = true;
    const tISO = window.APP_TODAY_ISO || '2026-05-31';
    Promise.all(months.map((mi) => {
      const mm = String(mi + 1).padStart(2, '0');
      const start = `${yearView}-${mm}-01`;
      if (start > tISO) return Promise.resolve([mi, null]);
      let end = `${yearView}-${mm}-${String(new Date(yearView, mi + 1, 0).getDate()).padStart(2, '0')}`;
      if (end > tISO) end = tISO;
      // Оплаченные берём из СНИМКОВ выплат этого месяца (payout_lessons — замороженная истина:
      // ставка на дату + надбавка), а не пересчитываем на фронте: пересчёт не знает надбавок и
      // разошёлся бы со снимком после смены ставки задним числом.
      const monthPayouts = hist.filter(p => (+String(p.periodStart).slice(5, 7) - 1) === mi);
      const snap = window.fetchPayoutLessons
        ? Promise.all(monthPayouts.map(p => window.fetchPayoutLessons(p.id).catch(() => [])))
            .then(lists => lists.flat())
            .catch(() => [])
        : Promise.resolve([]);
      return Promise.all([window.fetchPayoutPreview(staff.id, start, end), snap])
        .then(([pv, paidRows]) => [mi, { ...pv, paidRows }])
        .catch(() => [mi, null]);
    })).then((pairs) => { if (!alive) return; const m = {}; pairs.forEach(([mi, pv]) => { if (pv) m[mi] = pv; }); setConflictPv(m); });
    return () => { alive = false; };
    // payoutVer — бампится App'ом после создания выплаты; reloadKey — после сторно. Без них превью
    // залипало: только что оплаченные занятия продолжали висеть в «Конфликтах» как «Доплатить».
  }, [staff.id, staff.role, yearView, reloadKey, payoutVer]);

  // Заявки преподавателя на смену ФИО/телефона (личность ведёт админ — он и решает).
  // Список ожидающих тянем с сервера; после решения значение применяется на бэкенде и
  // преподавателю уходит уведомление.
  const [reqs, setReqs] = React.useState([]);
  const [reqBusy, setReqBusy] = React.useState(null);   // id заявки, по которой идёт запрос
  const [rejectFor, setRejectFor] = React.useState(null); // заявка → модалка отказа
  const [reqVer, setReqVer] = React.useState(0);
  React.useEffect(() => {
    if (staff.id == null || !window.apiCall) { setReqs([]); return; }
    let alive = true;
    window.apiCall('/staff/change-requests?staffId=' + staff.id)
      .then((r) => { if (alive) setReqs(r.requests || []); })
      .catch(() => { if (alive) setReqs([]); }); // нет права — просто нет плашки
    return () => { alive = false; };
  }, [staff.id, reqVer]);
  const decideReq = async (req, approve, note) => {
    setReqBusy(req.id);
    try {
      await window.apiCall('/staff/change-requests/' + req.id, {
        method: 'PATCH', body: JSON.stringify({ approve, note: note || '' }),
      });
      if (approve) { // витрину обновляем сразу, чтобы карточка не показывала старое значение
        if (req.field === 'full_name') staff.name = req.newValue;
        if (req.field === 'phone') staff.contact = req.newValue;
      }
      setRejectFor(null);
      setReqVer((v) => v + 1);
      force();
      toast(approve ? 'Заявка одобрена · ' + req.fieldLabel + ' обновлено' : 'Заявка отклонена');
    } catch (e) {
      toast('Не удалось: ' + (e.message || 'ошибка'));
    } finally { setReqBusy(null); }
  };

  // Students
  const [addOpen, setAddOpen] = React.useState(false);

  if (!staff) return null;
  const rm = SC_ROLE[staff.role] || SC_ROLE.teacher;
  const sm = SC_STATUS[staff.status] || SC_STATUS.active;
  const isTeacher = staff.role === 'teacher';
  const rate = staff.rate || 0;

  const payouts = (window.CRM_DATA.PAYOUT_HISTORY || []).filter(p => p.teacherId === staff.id);
  const activePayouts = payouts.filter(p => !p.voidedAt); // без сторнированных — для статистики/«выплачено»/конфликтов
  const paidTotal = activePayouts.filter(h => h.status === 'paid').reduce((a, b) => a + b.amount, 0);
  // «Показатели за месяц» → «Выплачено»: только выплаты ТЕКУЩЕГО месяца (по месяцу НАЧИСЛЕНИЯ —
  // periodStart, как в помесячной таблице и в Финансах). Раньше сюда шёл paidTotal = за всё время.
  const scCurMi = +scToday.slice(5, 7) - 1, scCurY = +scToday.slice(0, 4);
  const paidThisMonth = activePayouts
    .filter(h => h.status === 'paid' && h.periodStart
      && (+String(h.periodStart).slice(5, 7) - 1) === scCurMi && (+String(h.periodStart).slice(0, 4)) === scCurY)
    .reduce((a, b) => a + b.amount, 0);
  const tmplAll = (window.CRM_DATA.SCHEDULE && window.CRM_DATA.SCHEDULE[staff.id]) || [];
  const expand = window.expandSchedule || (() => []);

  // ---- РЕАЛЬНЫЕ занятия преподавателя из леджеров учеников (единый источник: статистика,
  //      нагрузка, история, календарь). Меняется статус занятия у ученика — меняется и здесь.
  const lessonsOfClient = window.ensureClientLessons || ((c) => (window.genClientLessons ? window.genClientLessons(c, Array.from(new Set([c.teacher, ...(c.pastTeachers || [])].filter(Boolean)))) : []));
  const realLessons = [];
  if (isTeacher) {
    // У преподавателя — РЕАЛЬНЫЕ занятия за год из API (day/start/end/status/who уже в нужной форме,
    // статусы в вокабуляре кита). Личные события не считаем.
    overviewLessons.forEach(l => { if (l.kind !== 'personal') {
      const p = (l.dateISO || '').split('-');
      const h = Math.floor(l.start || 0), mn = Math.round(((l.start || 0) - h) * 60);
      realLessons.push({ ...l, weekday: l.day, month: +(l.dateISO || '').slice(5, 7) - 1, clientId: null,
        dateLabel: p.length === 3 ? `${+p[2]} ${SC_MONTHS[+p[1] - 1].toLowerCase()}` : (l.dateISO || ''),
        time: `${String(h).padStart(2, '0')}:${String(mn).padStart(2, '0')}` });
    } });
  } else {
    (window.CRM_DATA.CLIENTS || []).forEach(c => { (lessonsOfClient(c) || []).forEach(l => {
      if (l.teacher !== staff.name) return;
      const p = (l.dateISO || '').split('-');
      const wd = p.length === 3 ? (new Date(+p[0], +p[1] - 1, +p[2]).getDay() + 6) % 7 : 0;
      const sh = l.time ? parseInt(String(l.time).split(':')[0], 10) : null;
      realLessons.push({ ...l, who: c.child, clientId: c.id, weekday: wd, day: wd, start: sh != null ? sh : null, end: sh != null ? sh + 1 : null, group: false });
    }); });
  }
  const realIn = (start, end) => realLessons.filter(l => l.dateISO >= start && l.dateISO <= end);

  // ---- Показатели за текущий месяц (май 2026) — из реальных занятий ----
  const monthRows = realIn(scMonStart, scMonEnd);
  const mLessons = monthRows.filter(l => l.status === 'paid' || l.status === 'debt' || l.status === 'noshow').length;
  const mHours = mLessons; // занятие = 1 час (час начала известен, длительность 1ч)
  const mPaid = monthRows.filter(l => l.status === 'paid').length;
  const mNoshow = monthRows.filter(l => l.status === 'noshow').length;

  // ---- Нагрузка (пиковая) по дням и часам — фактические занятия за выбранный месяц, либо весь период ----
  const loadSrc = (monthView != null
    ? realIn(`${yearView}-${String(monthView + 1).padStart(2, '0')}-01`, `${yearView}-${String(monthView + 1).padStart(2, '0')}-${new Date(yearView, monthView + 1, 0).getDate()}`)
    : realLessons
  ).filter(l => (!ovStatus || ((ovStatus === 'conducted' ? (l.status === 'paid' || l.status === 'debt') : l.status === ovStatus))) && l.start != null);
  const byDay = SC_WDAYS.map((_, di) => loadSrc.filter(l => l.day === di).length);
  // Все 24 часа: раньше показывали жёстко 09..22 и занятия вне этого окна просто не было видно.
  // Полоса прокручивается по горизонтали, при смене периода сама подъезжает к первому занятому часу.
  const hourSlots = Array.from({ length: 24 }, (_, i) => i); // 00..23
  const byHour = hourSlots.map(h => loadSrc.filter(l => l.start <= h && h < l.end).length);
  const loadScopeLabel = monthView != null ? `${SC_MONTHS[monthView]} ${yearView}` : 'весь период';
  // Ключ периода → при смене месяца/года/статуса окна нагрузки/статусов ремоунтятся и анимируются заново.
  // Направление (ovSlide) даёт въезд со стороны нажатой стрелки — как дата в подразделе «Занятия».
  const ovPeriodKey = `${monthView}|${yearView}|${ovStatus}`;
  const ovDirAnim = (ovSlide > 0 ? 'om-date-in-left' : ovSlide < 0 ? 'om-date-in-right' : 'om-fade-in') + ' 300ms cubic-bezier(0.22, 1, 0.36, 1) both';

  // ---- Помесячная работа (год) ----
  // Занятия/часы — фактические из расписания (от ставки не зависят).
  // ВЫПЛАТА за месяц берётся из СФОРМИРОВАННЫХ выплат (PAYOUT_HISTORY) — это неизменяемые
  // снимки, созданные кнопкой «Выплатить». Менять цену занятия / ставку задним числом
  // и тем самым пересчитывать прошлые выплаты — нельзя. Если выплата за месяц ещё не
  // сформирована, показываем ОЦЕНКУ (× текущая ставка) с пометкой «≈».
  const yearRows = realIn(`${yearView}-01-01`, `${yearView}-12-31`).filter(l => !ovStatus || (ovStatus === 'conducted' ? (l.status === 'paid' || l.status === 'debt') : l.status === ovStatus));
  const payoutByMonth = {};
  activePayouts.forEach(p => {
    const pm = parsePeriodMonth(p.period);
    if (pm == null) return;
    const py = (parsePayDate(p.payDate) || {}).year;
    const y = py != null ? py : yearView; // у запланированных payDate ещё нет — относим к выбранному году
    if (y !== yearView) return;
    const slot = payoutByMonth[pm] || (payoutByMonth[pm] = { amount: 0, status: 'paid' });
    slot.amount += p.amount;
    if (p.status !== 'paid') slot.status = 'planned';
  });
  const monthAgg = SC_MONTHS.map((label, mi) => {
    const rows = yearRows.filter(r => r.month === mi);
    const formed = payoutByMonth[mi];
    // Наработано — сумма по занятиям (ставка на дату). Выплачено — сумма сформированных выплат.
    const earned = window.rateOn ? rows.reduce((a, r) => a + window.rateOn(staff, r.dateISO), 0) : rows.length * rate;
    const paid = formed ? formed.amount : 0;
    return {
      label, lessons: rows.length, hours: rows.reduce((a, r) => a + (r.end - r.start), 0),
      payout: formed ? formed.amount : earned,
      earned, paid,
      payoutFormed: !!formed,
      payoutStatus: formed ? formed.status : (rows.length ? 'estimate' : 'none'),
    };
  });
  const yearTotals = monthAgg.reduce((a, m) => ({ lessons: a.lessons + m.lessons, hours: a.hours + m.hours, payout: a.payout + m.payout }), { lessons: 0, hours: 0, payout: 0 });

  // ---- Конфликты выплат: за месяц выплачено меньше, чем наработано (занятия добавлены задним числом).
  // По каждому месяцу с уже сформированной выплатой делим занятия по дате: пока накопленная
  // оплата укладывается в выплаченную сумму — «оплачены», дальше — «не оплачены педагогу».
  const payoutConflicts = SC_MONTHS.map((label, mi) => {
    const pv = conflictPv[mi];
    if (!pv || !pv.toPay || pv.toPay.length === 0) return null; // нет реально неоплаченных → нет конфликта
    const mm = String(mi + 1).padStart(2, '0');
    const unpaidIds = new Set(pv.toPay.map(t => t.lessonId));
    const unpaidLessons = pv.toPay.map(t => ({ id: t.lessonId, dateISO: t.dateISO, dateLabel: t.dateLabel, time: t.time, who: t.who, pay: t.pay }));
    // ОПЛАЧЕННЫЕ — из снимков выплат (реально уплаченные суммы: ставка на дату + надбавка).
    // Фолбэк (снимок не отдался) — прежний локальный расчёт по ставке на дату.
    const snapRows = (pv.paidRows || []).filter(l => l.dateISO && l.dateISO.slice(0, 7) === `${yearView}-${mm}`);
    const paidLessons = snapRows.length
      ? snapRows.map(l => ({ id: l.lessonId, dateISO: l.dateISO, dateLabel: l.dateLabel, time: l.time, who: l.who, pay: l.amount, bonus: l.bonus || 0 }))
      : realIn(`${yearView}-${mm}-01`, `${yearView}-${mm}-${new Date(yearView, mi + 1, 0).getDate()}`)
          .filter(l => (l.status === 'paid' || l.status === 'debt' || l.status === 'noshow') && !unpaidIds.has(l.id))
          .map(l => ({ ...l, pay: window.rateOn ? window.rateOn(staff, l.dateISO) : rate }));
    const unpaidSum = pv.toPay.reduce((a, t) => a + (t.pay || 0), 0);
    const paid = paidLessons.reduce((a, l) => a + l.pay, 0);
    return { mi, label, year: yearView, paid, paidLessons, unpaidLessons, unpaidSum, payoutStatus: 'paid' };
  }).filter(Boolean);

  // ---- Статус-доля за ВЫБРАННЫЙ период (месяц/год из селектора «Нагрузка и статистика») ----
  // Раньше считалась за текущую неделю и не менялась при листании месяцев — теперь синхронна с графиками.
  const statusGroups = {};
  const statusSrc = isTeacher
    ? (monthView != null
        ? realIn(`${yearView}-${String(monthView + 1).padStart(2, '0')}-01`, `${yearView}-${String(monthView + 1).padStart(2, '0')}-${new Date(yearView, monthView + 1, 0).getDate()}`)
        : realLessons)
    : tmplAll;
  statusSrc.forEach(l => { statusGroups[l.status] = (statusGroups[l.status] || 0) + 1; });
  const totalTmpl = statusSrc.length;
  const statusPeriodLabel = isTeacher ? (monthView != null ? `${SC_MONTHS_FULL[monthView].toLowerCase()} ${yearView}` : `${yearView} год`) : 'неделю';

  // ---- Ученики преподавателя (из базы клиентов) ----
  const CLIENTS = window.CRM_DATA.CLIENTS || [];
  // Статистика по ученику строится ТОЛЬКО из занятий, помеченных как занятия с этим педагогом.
  // Источник — тот же генератор истории, что и в карточке ученика, поэтому цифры совпадают.
  const SC_MONTH_START = scMonStart, SC_MONTH_END = scMonEnd;
  // Для генерации ИСТОРИИ — только основной + прошлые педагоги (без только что добавленных co-teachers,
  // иначе им начисляются фантомные занятия задним числом).
  const teachersOf = (c) => Array.from(new Set([c.teacher, ...(c.pastTeachers || [])].filter(Boolean)));
  const lessonsOf = window.ensureClientLessons || ((c) => (window.genClientLessons ? window.genClientLessons(c, teachersOf(c)) : []));
  const myStats = {};
  if (isTeacher) {
    // Из РЕАЛЬНЫХ занятий года (overviewLessons), сгруппированных по имени ученика — счётчик
    // «занятий/мес» и «последнее» появляются СРАЗУ, не после открытия карточки ученика.
    const byWho = {};
    overviewLessons.forEach(l => { if (l.kind !== 'personal' && l.who) (byWho[l.who] = byWho[l.who] || []).push(l); });
    CLIENTS.forEach(c => {
      // ВСЕ занятия ученика, кроме отменённых (планируемые тоже считаем — чтобы «занятий/мес» и
      // «последнее» обновлялись сразу после добавления, а не только для проведённых).
      const all = (byWho[c.child] || []).filter(l => l.status !== 'cancelled');
      const monthCount = all.filter(l => l.dateISO >= SC_MONTH_START && l.dateISO <= SC_MONTH_END).length;
      // «Последнее занятие» = самое свежее занятие, которое уже наступило (dateISO ≤ сегодня).
      const lastISO = all.filter(l => l.dateISO && l.dateISO <= scToday).reduce((mx, l) => (l.dateISO > mx ? l.dateISO : mx), '');
      myStats[c.id] = { monthCount, lastISO: lastISO || null };
    });
  } else {
    CLIENTS.forEach(c => {
      const mine = (lessonsOf(c) || []).filter(l => l.teacher === staff.name && l.status !== 'cancelled');
      const monthCount = mine.filter(l => l.dateISO >= SC_MONTH_START && l.dateISO <= SC_MONTH_END).length;
      const lastISO = mine.filter(l => l.dateISO && l.dateISO <= scToday).reduce((mx, l) => (l.dateISO > mx ? l.dateISO : mx), '');
      myStats[c.id] = { monthCount, lastISO: lastISO || null };
    });
  }
  const studentStat = (c) => myStats[c.id] || { monthCount: 0, lastISO: null };

  // Текущий ученик педагога: если он основной преподаватель ИЛИ входит в список преподавателей ученика.
  const isCurrentOf = (c) => c.teacher === staff.name || (c.teachers || []).includes(staff.name);
  const students = CLIENTS.filter(c => isCurrentOf(c) && !c.archived);
  const frequent = students.slice().filter(s => studentStat(s).monthCount > 0).sort((a, b) => studentStat(b).monthCount - studentStat(a).monthCount);
  // «Давно не занимались» — не было занятия с этим педагогом больше 2 недель (14 дней).
  const inactive = students.slice().filter(s => { const n = daysSince(studentStat(s).lastISO); return n != null && n >= 14; }).sort((a, b) => daysSince(studentStat(b).lastISO) - daysSince(studentStat(a).lastISO));
  // «Занимались раньше» — бывшие ученики этого педагога (в архиве или у другого преподавателя).
  const formerStudents = CLIENTS.filter(c => (c.pastTeachers || []).includes(staff.name) && !isCurrentOf(c))
    .sort((a, b) => (daysSince(studentStat(a).lastISO) == null ? 1e9 : daysSince(studentStat(a).lastISO)) - (daysSince(studentStat(b).lastISO) == null ? 1e9 : daysSince(studentStat(b).lastISO)));
  const assignable = CLIENTS.filter(c => !c.archived && !isCurrentOf(c));

  // ---- История занятий (период) — из тех же реальных занятий ----
  // Источник истории: у преподавателя — РЕАЛЬНЫЕ занятия из API (apiLessons, тот же money-free
  // эндпоинт, что календарь-зеркало), приведённые к строкам таблицы (dateLabel + weekday=day).
  // У не-преподавателя — прежний realLessons (пусто/леджеры). Статусы в вокабуляре кита уже пришли.
  const SC_MON_ABBR = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
  const apiHistory = apiLessons
    .filter(l => l.kind !== 'personal') // личные события препода — не занятия ученика
    .map(l => {
      const p = (l.dateISO || '').split('-');
      // time («HH:MM» из start) и amount (₽ из копеек) нужны «Карточке занятия», которая
      // открывается кликом по строке — она ждёт те же поля, что и в карточке ученика.
      const hh = l.start != null ? String(Math.floor(l.start)).padStart(2, '0') + ':' + String(Math.round((l.start % 1) * 60)).padStart(2, '0') : null;
      return {
        ...l, weekday: l.day,
        dateLabel: p.length === 3 ? `${+p[2]} ${SC_MON_ABBR[+p[1] - 1]}` : (l.dateISO || ''),
        time: l.time || hh,
        amount: l.amount != null ? l.amount : (l.amountKopecks != null ? Math.round(l.amountKopecks / 100) : null),
      };
    });
  const myLessons = isTeacher ? apiHistory : realLessons;
  const studentNames = Array.from(new Set(myLessons.map(l => l.who).filter(Boolean)));
  const historyConductOf = (st) => (st === 'paid' || st === 'debt') ? 'conducted' : st;
  const history = myLessons
    .filter(l => (!lessonStatus.length || lessonStatus.indexOf(historyConductOf(l.status)) >= 0) && (!lessonStudent.length || lessonStudent.indexOf(l.who) >= 0) && l.dateISO >= range.start && l.dateISO <= range.end)
    // Сначала по дате (свежие сверху), потом по времени внутри дня (позднее — выше): строго обратно-хронологически.
    .sort((a, b) => a.dateISO !== b.dateISO ? (a.dateISO < b.dateISO ? 1 : -1) : ((b.start || 0) - (a.start || 0)));

  const th = { textAlign: 'left', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '0 16px 12px', whiteSpace: 'nowrap' };
  const td = { padding: '12px 16px', borderTop: '1px solid var(--border)', fontSize: 14, color: 'var(--text)', verticalAlign: 'middle' };
  // «Оплата преп.» = текущая ставка ЭТОГО преподавателя на сегодня (как в карточке клиента),
  // а не устаревшее сохранённое c.teacherPay — чтобы данные совпадали с карточкой ученика.
  const teacherPayNow = window.rateOn ? window.rateOn(staff, window.APP_TODAY_ISO || '2026-05-31') : (staff.rate || 0);

  // ---- Аналитика менеджера: сколько заявок взял в работу и сколько конвертировал ----
  const LEADS = window.CRM_DATA.LEADS || [];
  const mgrMatch = (m) => m && (m === staff.tg || m === staff.login || m === staff.name);
  const mgrLeadsLinked = LEADS.filter(l => mgrMatch(l.manager)).length; // заявок связано (взято в работу)
  const mgrConverted = CLIENTS.filter(c => mgrMatch(c.manager)).length; // конвертировано в клиентов
  const mgrActive = CLIENTS.filter(c => mgrMatch(c.manager) && c.status === 'active').length;
  const mgrConvRate = (mgrLeadsLinked + mgrConverted) > 0 ? Math.round(mgrConverted / (mgrLeadsLinked + mgrConverted) * 100) : 0;

  const TABS = [{ id: 'overview', label: 'Обзор' }];
  if (isTeacher) TABS.push({ id: 'lessons', label: 'Занятия' }, { id: 'students', label: 'Ученики' }, { id: 'payouts', label: 'Выплаты' });

  // Статусы ПРОВЕДЕНИЯ для фильтра истории (не оплата): Проведено / Не пришёл / Запланировано / Отменено.
  const conductOpts = [
    { value: 'conducted', label: 'Проведено' },
    { value: 'noshow', label: 'Не пришёл' },
    { value: 'planned', label: 'Запланировано' },
    { value: 'cancelled', label: 'Отменено' },
  ];

  const assignMany = (ids) => {
    if (!ids || !ids.length) return;
    ids.forEach(id => { const c = CLIENTS.find(x => x.id === id); if (c) {
      lessonsOf(c); // заморозить историю с прежней привязкой ДО добавления педагога
      // Мульти-педагог: ДОБАВЛЯЕМ преподавателя к списку, а не заменяем (ученик может быть у нескольких).
      const set = new Set((c.teachers && c.teachers.length) ? c.teachers : (c.teacher ? [c.teacher] : []));
      set.add(staff.name);
      c.teachers = Array.from(set);
      c.teacher = c.teachers[0];
      // Цена занятия по педагогу ОБЯЗАТЕЛЬНА: при закреплении сразу проставляем дефолт (редактируется в карточке клиента).
      c.teacherPrices = { ...(c.teacherPrices || {}) };
      if (!(parseInt(c.teacherPrices[staff.name], 10) > 0)) c.teacherPrices[staff.name] = window.DEFAULT_LESSON_PRICE || 1800;
      c.pastTeachers = (c.pastTeachers || []).filter(t => t !== staff.name);
      c.archived = false;
    } });
    setAddOpen(false); force();
    // ПЕРСИСТ: раньше закрепление жило только в памяти страницы — после F5 ученик пропадал,
    // и в кабинете преподавателя не появлялся (связки в БД не возникало).
    persistTeacherLink(ids, 'attach');
  };

  /** Закрепление/открепление ученика за ЭТИМ преподавателем на сервере (PATCH /clients/:id).
   *  Шлём ПОЛНЫЙ список преподов ученика: бэкенд создаст недостающие связки и закроет убранные
   *  (занятия по закрытой связке остаются — история не трётся). */
  const persistTeacherLink = async (ids, mode) => {
    const list = Array.isArray(ids) ? ids : [ids];
    let ok = 0;
    for (const cid of list) {
      const c = CLIENTS.find(x => x.id === cid);
      if (!c) continue;
      const teachers = (c.teachers || []).filter(Boolean);
      // Предмет новой связки — первый общий у педагога и ученика (иначе связка без предмета).
      const subs = { ...(c.teacherSubjects || {}) };
      if (mode === 'attach' && !(subs[staff.name] || []).length) {
        const mine = staff.subjects || [];
        const match = (c.directions || []).find(d => mine.includes(d)) || mine[0] || null;
        // Прикрепить без предмета нельзя: непонятно, что педагог ведёт, и в кабинете
        // ученик появится без предмета. Подобрать нечего → честно говорим, что делать.
        if (!match) {
          toast(`У «${staff.name.split(/\s+/)[0]}» не задан предмет — добавьте его в профиле педагога (Сотрудники → Предметы)`);
          // откатываем локальное прикрепление, чтобы экран не расходился с базой
          c.teachers = (c.teachers || []).filter(t => t !== staff.name);
          c.teacher = (c.teachers || [])[0] || null;
          continue;
        }
        subs[staff.name] = [match];
      }
      if (mode === 'detach') delete subs[staff.name];
      try {
        await window.apiCall('/clients/' + cid, { method: 'PATCH', body: JSON.stringify({ teachers, teacherSubjects: subs }) });
        c.teacherSubjects = subs;
        ok++;
      } catch (e) {
        toast('Не удалось сохранить: ' + (e.message || 'ошибка'));
      }
    }
    if (!ok) return;
    force();
    if (mode === 'attach') toast(ok === 1 ? 'Ученик закреплён за преподавателем' : `Закреплено учеников: ${ok}`);
  };
  // Добавление занятия педагогом: пишем в леджер выбранного ученика (тот же источник, что история).
  // Создание занятия НА БЭКЕНДЕ (POST /lessons) — как в карточке ученика. Если задан факт
  // (провёл/пропуск/отмена) — сразу переводим статус через PATCH. Затем перечитываем данные.
  const addLessonToStudent = async (draft) => {
    const c = students.find(x => x.child === draft.who) || CLIENTS.find(x => x.child === draft.who && isCurrentOf(x));
    if (!c) { toast('Выберите ученика'); return; }
    try {
      const nt = scNormTime(draft.time); // «9:00» → «09:00», иначе валидный ISO не собрать
      const at = new Date((draft.dateISO || '') + 'T' + (/^\d{2}:\d{2}$/.test(nt) ? nt : '17:00') + ':00');
      if (Number.isNaN(at.getTime())) { toast('Некорректная дата/время'); return; }
      const res = await window.apiCall('/lessons', { method: 'POST', body: JSON.stringify({
        clientId: c.id, teacherId: staff.id, scheduledAt: at.toISOString(), durationMinutes: 60,
      }) });
      const kindToStatus = { normal: 'conducted', noshow: 'noshow', cancelled: 'cancelled' };
      if (draft.type && kindToStatus[draft.type]) {
        const body = { status: kindToStatus[draft.type] };
        if (draft.type === 'noshow') { body.noshowCharged = (draft.penalty || 0) > 0; if (body.noshowCharged) body.penaltyKopecks = Math.round(draft.penalty * 100); }
        await window.apiCall('/lessons/' + res.id + '/status', { method: 'PATCH', body: JSON.stringify(body) });
      }
      setNewLesson(null);
      // МГНОВЕННО отражаем занятие в статистике: добавляем в overviewLessons сразу (счётчик «в этом
      // мес.» и окно «Давно не занимались» обновляются, не дожидаясь асинхронного рефетча).
      const kitStatus = draft.type === 'normal' ? 'paid' : draft.type === 'noshow' ? 'noshow' : draft.type === 'cancelled' ? 'cancelled' : 'planned';
      const od = new Date((draft.dateISO || '') + 'T00:00:00');
      const startH = (() => { const m = (/^\d{2}:\d{2}$/.test(nt) ? nt : '17:00').split(':'); return +m[0] + (+m[1]) / 60; })();
      setOverviewLessons(prev => [...prev.filter(l => l.id !== res.id), { id: res.id, who: c.child, dateISO: draft.dateISO, status: kitStatus, kind: 'lesson', day: (od.getDay() + 6) % 7, start: startH, end: startH + 1, group: false }]);
      setReloadKey(v => v + 1); // + сверить с сервером (перечитать историю/обзор/счётчики)
      toast('Занятие добавлено');
    } catch (e) { toast('Не удалось добавить занятие: ' + (e.message || 'ошибка')); }
  };
  // Сохранение из «Карточки занятия» (LessonCard) на БЭКЕНД: создание (POST) или правка (PATCH).
  // Ученик — по clientId из карточки. Проведённое: PATCH берёт только тему/ДЗ (бэк отклонит деньги/время).
  const scDbStatusOf = (paymentStatus) => ({ paid: 'conducted', debt: 'conducted', planned: 'scheduled', noshow: 'noshow', cancelled: 'cancelled' }[paymentStatus] || 'scheduled');
  const saveLessonFromCard = async (draft) => {
    const c = students.find(x => x.id === draft.clientId) || students.find(x => x.child === draft.who);
    if (!c) { toast('Выберите ученика'); return; }
    try {
      const nt = scNormTime(draft.time);
      const at = new Date((draft.dateISO || '') + 'T' + (/^\d{2}:\d{2}$/.test(nt) ? nt : '17:00') + ':00');
      if (Number.isNaN(at.getTime())) { toast('Некорректная дата/время'); return; }
      const amtRub = parseInt(String(draft.amount || '').replace(/\D/g, ''), 10);
      if (draft.id != null) {
        // Текущее состояние занятия: обзор (год) ИЛИ история выбранного периода — карточка
        // открывается кликом по строке истории, которая может лежать вне окна обзора.
        const cur = overviewLessons.find(l => l.id === draft.id) || apiLessons.find(l => l.id === draft.id) || {};
        const isPlanned = cur.status === 'planned';
        const patch = { topic: draft.topic || '', homework: draft.homework || '' };
        if (isPlanned) { patch.scheduledAt = at.toISOString(); if (amtRub > 0) patch.amountKopecks = amtRub * 100; }
        if (draft.teacherBonusRubles != null && draft.teacherBonusRubles !== (cur.teacherBonusRubles || 0)) patch.teacherBonusRubles = draft.teacherBonusRubles;
        await window.apiCall('/lessons/' + draft.id, { method: 'PATCH', body: JSON.stringify(patch) });
        const want = { normal: 'conducted', noshow: 'noshow', cancelled: 'cancelled', future: 'scheduled' }[draft.type];
        if (want && want !== scDbStatusOf(cur.status)) { const body = { status: want }; if (draft.type === 'noshow') { body.noshowCharged = (draft.penalty || 0) > 0; if (body.noshowCharged) body.penaltyKopecks = Math.round(draft.penalty * 100); } await window.apiCall('/lessons/' + draft.id + '/status', { method: 'PATCH', body: JSON.stringify(body) }); }
        setCard(null); setReloadKey(v => v + 1); toast('Занятие обновлено');
        return;
      }
      const res = await window.apiCall('/lessons', { method: 'POST', body: JSON.stringify({
        clientId: c.id, teacherId: staff.id, scheduledAt: at.toISOString(), durationMinutes: 60,
        amountKopecks: amtRub > 0 ? amtRub * 100 : undefined,
        teacherBonusRubles: draft.teacherBonusRubles || undefined,
        topic: draft.topic || undefined, homework: draft.homework || undefined,
      }) });
      const kindToStatus = { normal: 'conducted', noshow: 'noshow', cancelled: 'cancelled' };
      if (draft.type && kindToStatus[draft.type]) { const body = { status: kindToStatus[draft.type] }; if (draft.type === 'noshow') { body.noshowCharged = (draft.penalty || 0) > 0; if (body.noshowCharged) body.penaltyKopecks = Math.round(draft.penalty * 100); } await window.apiCall('/lessons/' + res.id + '/status', { method: 'PATCH', body: JSON.stringify(body) }); }
      setCard(null);
      const kitStatus = draft.type === 'normal' ? 'paid' : draft.type === 'noshow' ? 'noshow' : draft.type === 'cancelled' ? 'cancelled' : 'planned';
      const od = new Date((draft.dateISO || '') + 'T00:00:00');
      const startH = (() => { const m = (/^\d{2}:\d{2}$/.test(nt) ? nt : '17:00').split(':'); return +m[0] + (+m[1]) / 60; })();
      setOverviewLessons(prev => [...prev.filter(l => l.id !== res.id), { id: res.id, who: c.child, dateISO: draft.dateISO, status: kitStatus, kind: 'lesson', day: (od.getDay() + 6) % 7, start: startH, end: startH + 1, group: false }]);
      setReloadKey(v => v + 1);
      toast('Занятие добавлено');
    } catch (e) { toast('Не удалось сохранить занятие: ' + (e.message || 'ошибка')); }
  };
  const unassignStudent = (c) => {
    lessonsOf(c); // фиксируем историю до открепления
    const list = ((c.teachers && c.teachers.length) ? c.teachers : (c.teacher ? [c.teacher] : [])).filter(t => t !== staff.name);
    c.teachers = list;
    c.teacher = list[0] || null;
    c.pastTeachers = Array.from(new Set([...(c.pastTeachers || []), staff.name]));
    force(); toast(`Ученик «${c.child}» откреплён · перенесён в «Занимались раньше»`);
    persistTeacherLink(c.id, 'detach');   // связка закрывается на сервере, занятия остаются
  };
  const removeFromHistory = (c) => {
    c.pastTeachers = (c.pastTeachers || []).filter(t => t !== staff.name);
    force(); toast(`«${c.child}» убран из истории педагога`);
  };

  const rangeLabel = (() => {
    const f = (s) => { const [, m, d] = s.split('-'); return `${+d} ${['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек'][+m - 1]}`; };
    return `${f(range.start)} – ${f(range.end)}`;
  })();

  // ЛК: вход по impersonation-токену (пароль НЕ нужен — его и не отдаёт API). Нужен лишь
  // аккаунт: если у препода нет логина — подсвечиваем и трясём карточку «Доступ в кабинет».
  const openCabinet = () => {
    if (!login.trim()) {
      setTab('overview');
      setAccessError(true);
      setAccessShake(k => k + 1);
      setTimeout(() => {
        const sc = scrollAreaRef.current, card = accessRef.current;
        if (sc && card) sc.scrollTo({ top: sc.scrollTop + card.getBoundingClientRect().top - sc.getBoundingClientRect().top - 16, behavior: 'smooth' });
      }, 60);
      toast('У преподавателя нет аккаунта — задайте логин в «Доступ в кабинет»');
      return;
    }
    if (onOpenCabinet) onOpenCabinet(staff);
    else toast('Открываю личный кабинет преподавателя…');
  };

  return (
    <div className="om-fade-in" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
      {/* Top bar */}
      <div style={{ padding: '16px 32px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 16, rowGap: 12, flexWrap: 'wrap', background: 'var(--bg)' }}>
        <button onClick={onClose} title="Назад"
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}
          style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '8px 13px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, transition: 'background var(--dur-fast) var(--ease)' }}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 6l-6 6 6 6"/></svg>К сотрудникам
        </button>
        <Avatar animal={staff.animal} tone={staff.tone} size={44} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <h1 title={staff.name} style={{ fontSize: 22, fontWeight: 700, letterSpacing: '-0.4px', margin: 0, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{staff.name}</h1>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 4 }}>
            <span style={{ padding: '2px 9px', borderRadius: 'var(--radius-pill)', fontSize: 11.5, fontWeight: 600, background: rm.bg, color: rm.fg, flex: 'none' }}>{rm.label}</span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12.5, fontWeight: 600, color: sm.fg, flex: 'none' }}><span style={{ width: 7, height: 7, borderRadius: '50%', background: sm.dot }} />{sm.label}</span>
            <span style={{ fontSize: 13, color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{staff.position}{staff.tg ? ' · ' + staff.tg : ''}</span>
          </div>
        </div>
        {isTeacher && <Button variant="secondary" icon="external-link" onClick={openCabinet}>Личный кабинет</Button>}
        <Button variant="secondary" icon="edit-3" onClick={() => onEdit && onEdit(staff)}>Редактировать</Button>
        {isTeacher && <Button variant="primary" icon="wallet" onClick={() => onPayout && onPayout(staff)}>Выплатить</Button>}
      </div>

      {/* Заявки преподавателя на смену ФИО/телефона — решает админ. Плашки нет, если заявок нет. */}
      {reqs.length > 0 && (
        <div style={{ padding: '14px 32px 0', display: 'flex', flexDirection: 'column', gap: 8 }}>
          {reqs.map(r => (
            <div key={r.id} className="om-card-in" style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', border: '1px solid #E8DCC0', borderRadius: 'var(--radius-md)', background: 'var(--gold-soft)' }}>
              <Icon name="edit-3" size={16} style={{ color: 'var(--gold-ink)', flex: 'none' }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--gold-ink)' }}>Заявка на смену: {r.fieldLabel}</div>
                <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  <span style={{ textDecoration: 'line-through', color: 'var(--text-dim)' }}>{r.oldValue || '—'}</span>
                  {' → '}<b style={{ color: 'var(--text)' }}>{r.newValue}</b>
                  {r.reason ? <span style={{ color: 'var(--text-dim)' }}> · «{r.reason}»</span> : null}
                </div>
              </div>
              <Button variant="ghost" size="sm" onClick={() => setRejectFor(r)} disabled={reqBusy === r.id}>Отклонить</Button>
              <Button variant="primary" size="sm" icon="check" onClick={() => decideReq(r, true)} disabled={reqBusy === r.id}>
                {reqBusy === r.id ? <span className="om-spin"><Icon name="loader" size={14} /></span> : 'Одобрить'}
              </Button>
            </div>
          ))}
        </div>
      )}

      {/* Tabs */}
      <div style={{ padding: '14px 32px 0', borderBottom: '1px solid var(--border)', background: 'var(--bg)', display: 'flex', gap: 4 }}>
        {TABS.map(t => (
          <button key={t.id} onClick={() => setTab(t.id)}
            onMouseEnter={e => { if (tab !== t.id) e.currentTarget.style.color = 'var(--text)'; }}
            onMouseLeave={e => { if (tab !== t.id) e.currentTarget.style.color = 'var(--text-dim)'; }}
            style={{ padding: '8px 14px 12px', border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 600, color: tab === t.id ? 'var(--brand-ink)' : 'var(--text-dim)', borderBottom: '2px solid ' + (tab === t.id ? 'var(--brand)' : 'transparent'), marginBottom: -1, transition: 'color var(--dur-fast) var(--ease)' }}>{t.label}</button>
        ))}
      </div>

      {/* key=tab → при переключении вкладки контент проявляется (om-fade-in) и скролл сбрасывается наверх. */}
      <div ref={scrollAreaRef} key={tab} className="om-fade-in" style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '24px 32px 40px' }}>
        {/* ================= ОБЗОР ================= */}
        {tab === 'overview' && (
          <div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: 22, alignItems: 'start' }}>
            {/* LEFT: данные + заметки */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
              <Card pad={0} style={{ overflow: 'hidden' }}>
                <div style={{ padding: '12px 16px', background: 'var(--bg-soft)', borderBottom: '1px solid var(--border)' }}><Overline style={{ color: 'var(--text-secondary)' }}>Данные</Overline></div>
                <div style={{ padding: '4px 16px 14px' }}>
                  <SCRow label="Должность" value={staff.position} />
                  {isTeacher && <SCRow label="Ставка" value={rate ? fmtNum(rate) + ' ₽ / занятие' : null} mono />}
                  {staff.subjects && staff.subjects.length > 0 && <SCRow label="Предметы" value={staff.subjects.join(', ')} />}
                  <SCRow label="Телефон" value={staff.contact} mono onCopy={copyVal} />
                  <SCRow label="Telegram" value={staff.tg} mono onCopy={copyVal} />
                  <SCRow label="С нами с" value={staff.since} />
                </div>
              </Card>

              <div key={accessShake} ref={accessRef} style={{ borderRadius: 'var(--radius-md)', animation: accessShake ? 'om-shake 0.5s var(--ease)' : 'none', boxShadow: accessError ? '0 0 0 2px var(--coral)' : 'none', transition: 'box-shadow var(--dur) var(--ease)' }}>
                <Card pad={0} style={{ overflow: 'hidden', borderColor: accessError ? 'var(--coral)' : undefined }}>
                <div style={{ padding: '12px 16px', background: accessError ? 'var(--coral-bg)' : 'var(--bg-soft)', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 8, transition: 'background var(--dur) var(--ease)' }}>
                  <Icon name="lock" size={14} style={{ color: accessError ? 'var(--coral)' : 'var(--text-dim)', flex: 'none' }} />
                  <Overline style={{ color: accessError ? 'var(--coral)' : 'var(--text-secondary)', whiteSpace: 'nowrap' }}>Доступ в кабинет</Overline>
                </div>
                <div style={{ padding: '4px 16px 14px' }}>
                  <SecretRow label="Логин" value={login} placeholder="логин" error={accessError && !login.trim()} onSave={v => saveAccess({ login: v })} />
                  <SecretRow label="Пароль" value={password} placeholder="новый пароль" error={accessError && !password.trim()} onSave={v => saveAccess({ password: v })} />
                </div>
                </Card>
              </div>

              <Card pad={0} style={{ overflow: 'hidden' }}>
                <div style={{ padding: '12px 16px', background: 'var(--bg-soft)', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <Overline style={{ color: 'var(--text-secondary)' }}>{isTeacher ? 'О преподавателе' : 'Заметки'}</Overline>
                  <button onClick={() => setNotesEdit(v => !v)}
                    onMouseEnter={e => { e.currentTarget.style.color = 'var(--brand)'; }} onMouseLeave={e => { e.currentTarget.style.color = 'var(--brand-ink)'; }}
                    style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', fontSize: 12.5, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 5, transition: 'color var(--dur-fast) var(--ease)' }}>
                    <Icon name={notesEdit ? 'check' : 'pencil'} size={13} />{notesEdit ? 'Готово' : 'Изменить'}
                  </button>
                </div>
                <div style={{ padding: 14 }}>
                  {notesEdit ? (
                    <textarea autoFocus value={notes} onChange={e => setNotes(e.target.value)} placeholder="Заметки: опыт, особенности, договорённости…" rows={5}
                      style={{ width: '100%', boxSizing: 'border-box', resize: 'vertical', border: '1px solid var(--brand)', boxShadow: 'var(--shadow-focus)', borderRadius: 'var(--radius-sm)', padding: '10px 12px', fontFamily: 'var(--font-sans)', fontSize: 13.5, color: 'var(--text)', outline: 'none', background: 'var(--bg-card)' }} />
                  ) : (
                    <div style={{ fontSize: 13.5, lineHeight: 1.5, color: notes ? 'var(--text-secondary)' : 'var(--text-dim)', whiteSpace: 'pre-wrap' }}>{notes || 'Заметок пока нет. Нажмите «Изменить», чтобы добавить.'}</div>
                  )}
                </div>
              </Card>
            </div>

            {/* RIGHT: метрики */}
            {isTeacher ? (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
                {/* Показатели за месяц */}
                <Card pad={22}>
                  <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
                    <Overline>Показатели за месяц</Overline>
                    <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>{SC_MONTHS_FULL[+scToday.slice(5, 7) - 1]} {scToday.slice(0, 4)}</span>
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(92px, 1fr))', gap: 18, marginTop: 16 }}>
                    <SCStat label="Занятий" value={mLessons} />
                    <SCStat label="Часов" value={mHours} />
                    <SCStat label="Проведено" value={mPaid} color="var(--green)" />
                    <SCStat label="Не пришёл" value={mNoshow} color="var(--amber)" />
                    <SCStat label="Выплачено" value={fmtNum(paidThisMonth) + ' ₽'} color="var(--green)" sub={paidTotal !== paidThisMonth ? 'за всё время ' + fmtNum(paidTotal) + ' ₽' : null} />
                  </div>
                </Card>

                {/* Фильтры аналитики */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                  <Overline style={{ flex: 1 }}>Нагрузка и статистика</Overline>
                  <div style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    <Dropdown value={yearView} onChange={setYearView} options={[2024, 2025, 2026].map(y => ({ value: y, label: String(y) }))} width={96} />
                    <button onClick={() => { setOvSlide(-1); setMonthView(mv => mv == null ? 11 : (mv === 0 ? null : mv - 1)); }} title="Предыдущий месяц" style={{ width: 30, height: 30, 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={monthView} onChange={(v) => { setOvSlide(0); setMonthView(v); }} options={[{ value: null, label: 'Весь год' }].concat(SC_MONTHS_FULL.map((m, i) => ({ value: i, label: m })))} placeholder="Весь год" width={140} />
                    <button onClick={() => { setOvSlide(1); setMonthView(mv => mv == null ? 0 : (mv === 11 ? null : mv + 1)); }} title="Следующий месяц" style={{ width: 30, height: 30, 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>
                  <Dropdown value={ovStatus} onChange={setOvStatus} options={[{ value: null, label: 'Все статусы' }, { value: 'conducted', label: 'Проведено' }, { value: 'noshow', label: 'Не пришёл' }, { value: 'planned', label: 'Запланировано' }, { value: 'cancelled', label: 'Отменено' }]} placeholder="Все статусы" width={168} />
                </div>

                {/* Пиковая нагрузка по дням и часам.
                    minmax(0, 1fr): грид-элемент по умолчанию НЕ сжимается уже содержимого — из-за
                    этого полоса «нагрузки по часам» (24 колонки) распирала карточку на всю ширину.
                    Теперь колонка держит свою половину, а полоса внутри прокручивается. */}
                <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 20 }}>
                  <Card pad={22}>
                    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
                      <Overline>Нагрузка по дням недели</Overline>
                      <span style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>{loadScopeLabel}</span>
                    </div>
                    <div key={ovPeriodKey} style={{ animation: ovDirAnim }}><MiniBars labels={SC_WDAYS} values={byDay} unit="зан." /></div>
                  </Card>
                  <Card pad={22}>
                    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
                      <Overline>Нагрузка по часам</Overline>
                      <span style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>{loadScopeLabel}</span>
                    </div>
                    <div key={ovPeriodKey} style={{ animation: ovDirAnim }}><MiniBars labels={hourSlots.map(String)} values={byHour} unit="зан." scroll /></div>
                  </Card>
                </div>

                {/* Помесячная работа + статус-доля */}
                <div style={{ display: 'grid', gridTemplateColumns: '1.55fr 1fr', gap: 20, alignItems: 'start' }}>
                  <Card pad={0} style={{ overflow: 'hidden' }}>
                    <div style={{ padding: '16px 18px 12px', display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
                      <Overline style={{ whiteSpace: 'nowrap' }}>Помесячная работа · {yearView}</Overline>
                      <span style={{ fontSize: 12, color: 'var(--text-dim)', whiteSpace: 'nowrap' }}>ставка {fmtNum(rate)} ₽ · «≈» — оценка</span>
                    </div>
                    <div style={{ maxHeight: 320, overflowY: 'auto', overflowX: 'hidden' }}>
                      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                        <thead><tr>
                          <th style={{ ...th, padding: '0 18px 10px' }}>Месяц</th>
                          <th style={{ ...th, textAlign: 'right', padding: '0 18px 10px' }}>Занятий</th>
                          <th style={{ ...th, textAlign: 'right', padding: '0 18px 10px' }}>Часов</th>
                          <th style={{ ...th, textAlign: 'right', padding: '0 18px 10px' }}>К выплате</th>
                        </tr></thead>
                        <tbody>
                          {monthAgg.map((m, i) => (
                            <tr key={i} className="row-hover" onClick={() => setMonthView(mv => mv === i ? null : i)} style={{ cursor: 'pointer', background: monthView === i ? 'var(--brand-soft)' : 'transparent' }}>
                              <td style={{ ...td, padding: '10px 18px', fontWeight: 600, color: monthView === i ? 'var(--brand-ink)' : 'var(--text)' }}>{m.label}</td>
                              <td style={{ ...td, padding: '10px 18px', textAlign: 'right', fontFamily: 'var(--font-mono)', color: m.lessons ? 'var(--text)' : 'var(--text-dim)' }}>{m.lessons || '—'}</td>
                              <td style={{ ...td, padding: '10px 18px', textAlign: 'right', fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>{m.hours ? m.hours : '—'}</td>
                              <td title={m.payoutFormed ? (m.payoutStatus === 'planned' ? 'Выплата сформирована (запланирована)' : 'Выплачено') : (m.payout ? 'Оценка — выплата за месяц ещё не сформирована' : '')} style={{ ...td, padding: '10px 18px', textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 600, color: !m.payout ? 'var(--text-dim)' : m.payoutFormed ? (m.payoutStatus === 'planned' ? 'var(--amber-ink)' : 'var(--green-ink)') : 'var(--text-dim)' }}>
                                {m.payout ? (m.payoutFormed ? '' : '≈ ') + fmtNum(m.payout) + ' ₽' : '—'}
                                {/* Бейдж «к доплате» убран (репорт владельца): считался как
                                    наработано−выплачено и врал, когда занятия месяца закрыты выплатой
                                    другого периода. Реальная недоплата — в «Конфликтах выплат»
                                    (по снимку payout_lessons), не эвристикой. */}
                              </td>
                            </tr>
                          ))}
                        </tbody>
                        <tfoot>
                          <tr>
                            <td style={{ ...td, padding: '12px 18px', borderTop: '2px solid var(--border-strong)', fontWeight: 700 }}>Итого</td>
                            <td style={{ ...td, padding: '12px 18px', borderTop: '2px solid var(--border-strong)', textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700 }}>{yearTotals.lessons}</td>
                            <td style={{ ...td, padding: '12px 18px', borderTop: '2px solid var(--border-strong)', textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700 }}>{yearTotals.hours}</td>
                            <td style={{ ...td, padding: '12px 18px', borderTop: '2px solid var(--border-strong)', textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, color: 'var(--green)' }}>{fmtNum(yearTotals.payout)} ₽</td>
                          </tr>
                        </tfoot>
                      </table>
                    </div>
                  </Card>

                  <Card pad={22}>
                    <Overline>Занятия по статусам</Overline>
                    <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 4 }}>За {statusPeriodLabel}: <b style={{ color: 'var(--text)', fontFamily: 'var(--font-mono)' }}>{totalTmpl}</b></div>
                    <div key={ovPeriodKey} style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 16, animation: ovDirAnim }}>
                      {['paid', 'noshow', 'cancelled', 'planned'].map((k, i) => {
                        const cnt = statusGroups[k] || 0; const pct = totalTmpl ? Math.round(cnt / totalTmpl * 100) : 0; const s = scStatus(k);
                        return (
                          <div key={k}>
                            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, marginBottom: 5 }}>
                              <span style={{ color: 'var(--text-secondary)' }}>{s.label}</span>
                              <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{cnt} · {pct}%</span>
                            </div>
                            <div style={{ height: 9, borderRadius: 5, background: 'var(--bg-soft)', overflow: 'hidden' }}><div className="om-grow-right" style={{ width: pct + '%', height: '100%', background: s.bd, transition: 'width 300ms var(--ease)', animationDelay: (120 + i * 60) + 'ms' }} /></div>
                          </div>
                        );
                      })}
                    </div>
                  </Card>
                </div>
              </div>
            ) : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
                <Card pad={22}>
                  <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
                    <Overline>Работа с заявками</Overline>
                    <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>всё время</span>
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))', gap: 18, marginTop: 16 }}>
                    <SCStat label="Заявок связал" value={mgrLeadsLinked} />
                    <SCStat label="Конвертировал" value={mgrConverted} color="var(--green)" />
                    <SCStat label="Активных клиентов" value={mgrActive} color="var(--brand-ink)" />
                    <SCStat label="Конверсия" value={mgrConvRate + '%'} color="var(--gold-ink)" />
                  </div>
                  <div style={{ marginTop: 16 }}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, marginBottom: 5 }}>
                      <span style={{ color: 'var(--text-secondary)' }}>Воронка: связано → конвертировано</span>
                      <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{mgrConverted} / {mgrLeadsLinked + mgrConverted}</span>
                    </div>
                    <div style={{ height: 9, borderRadius: 5, background: 'var(--bg-soft)', overflow: 'hidden' }}><div style={{ width: mgrConvRate + '%', height: '100%', background: 'var(--green)', transition: 'width 300ms var(--ease)' }} /></div>
                  </div>
                  {(mgrLeadsLinked + mgrConverted) === 0 && <div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 12 }}>За этим менеджером ({staff.tg || '—'}) пока не закреплены заявки или клиенты.</div>}
                </Card>
              </div>
            )}
          </div>
        )}

        {/* ================= ЗАНЯТИЯ ================= */}
        {tab === 'lessons' && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
            {/* Filter bar */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <Overline style={{ flex: 1 }}>Занятия преподавателя</Overline>
              <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                {(() => { const navBtn = { width: 34, height: 34, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)', transition: 'background var(--dur-fast), border-color var(--dur-fast)' };
                  const hov = e => { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; };
                  const out = e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; };
                  return (<>
                    <button title="Предыдущий месяц" onClick={() => shiftMonth(-1)} onMouseEnter={hov} onMouseLeave={out} style={navBtn}><Icon name="chevron-left" size={16} /></button>
                    <div style={{ position: 'relative' }}>
                      <button onClick={() => setCalOpen(o => !o)}
                        onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; }}
                        onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
                        style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 12px', cursor: 'pointer', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, color: 'var(--text)', transition: 'background var(--dur-fast), border-color var(--dur-fast)' }}>
                        <Icon name="calendar" size={15} style={{ color: 'var(--text-dim)' }} />
                        {/* key=range.start → при листании span монтируется заново и въезжает со стороны,
                            противоположной нажатой стрелке (движение К стрелке). */}
                        <span key={range.start} className={dateSlide > 0 ? 'om-date-in-left' : dateSlide < 0 ? 'om-date-in-right' : ''}>{rangeLabel}</span>
                      </button>
                      {calOpen && <RangeCalendar value={range} onApply={(r) => { setDateSlide(0); setRange(r); }} onClose={() => setCalOpen(false)} />}
                    </div>
                    <button title="Следующий месяц" onClick={() => shiftMonth(1)} onMouseEnter={hov} onMouseLeave={out} style={navBtn}><Icon name="chevron-right" size={16} /></button>
                  </>);
                })()}
              </div>
              <Dropdown value={lessonStatus} onChange={setLessonStatus} options={conductOpts} placeholder="Статусы" width={172} multi />
              <Dropdown value={lessonStudent} onChange={setLessonStudent} options={studentNames.map(n => ({ value: n, label: n }))} placeholder="Ученики" width={184} searchable multi />
            </div>

            {/* История занятий */}
            <Card pad={0} style={{ overflow: 'hidden' }}>
              <div style={{ padding: '16px 16px 12px', display: 'flex', alignItems: 'center', gap: 10 }}>
                <Overline>История занятий</Overline>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-dim)' }}>{history.length} · {rangeLabel}</span>
                <div style={{ flex: 1 }} />
                <Button variant="secondary" size="sm" icon="plus" onClick={() => setCard({ lesson: null })}>Занятие</Button>
              </div>
              {/* Высота фиксирована (текущий максимум) — карточка не прыгает при листании месяцев. */}
              <div style={{ height: 360, overflowY: 'auto', overflowX: 'hidden' }}>
                {history.length === 0 ? <div style={{ padding: '24px 16px', color: 'var(--text-dim)', fontSize: 14 }}>За выбранный период занятий по фильтру нет.</div> : (
                  <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                    <thead><tr style={{ position: 'sticky', top: 0, background: 'var(--bg-card)', zIndex: 1 }}><th style={th}>Дата</th><th style={th}>Время</th><th style={th}>Ученик</th><th style={th}>Статус</th></tr></thead>
                    {/* key включает histVer → при каждом пролистывании tbody монтируется заново → строки анимируются сверху вниз (stagger i*18) */}
                    <tbody key={`${histVer}|${lessonStatus}|${lessonStudent}`}>
                      {history.map((l, i) => {
                        const s = scStatus(l.status);
                        const fmt = (h) => `${String(Math.floor(h)).padStart(2, '0')}:${h % 1 ? '30' : '00'}`;
                        return (
                          <tr key={i} className="row-hover om-row-in" onClick={() => setCard({ lesson: l })} style={{ animationDelay: Math.min(i * 18, 280) + 'ms', cursor: 'pointer' }}>
                            <td style={{ ...td, fontWeight: 600, whiteSpace: 'nowrap' }}>{l.dateLabel}<span style={{ color: 'var(--text-dim)', fontWeight: 500, marginLeft: 6 }}>{SC_WDAYS[l.weekday]}</span></td>
                            <td style={{ ...td, fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text-secondary)', whiteSpace: 'nowrap' }}>{l.time || (l.start != null ? fmt(l.start) + '–' + fmt(l.end) : '—')}</td>
                            <td style={td}>{l.who}{l.group && <span style={{ fontSize: 10, fontWeight: 700, padding: '1px 5px', borderRadius: 3, background: 'var(--bg-soft)', color: 'var(--text-dim)', marginLeft: 6 }}>ГР</span>}</td>
                            <td style={td}><span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 600, color: s.fg }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: s.bd }} />{s.label}</span></td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                )}
              </div>
            </Card>

            {/* Личный календарь преподавателя */}
            <div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
                <Overline>Личный календарь</Overline>
                <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>зеркало кабинета преподавателя</span>
              </div>
              <div style={{ background: '#faf6ef', border: '1px solid #eae3d5', borderRadius: 14, padding: '14px 16px' }}>
                <TeacherCalendar embedded teacherId={staff.id} today={new Date()} onToast={toast} />
              </div>
            </div>
          </div>
        )}

        {/* ================= УЧЕНИКИ ================= */}
        {tab === 'students' && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 10 }}>
              <Overline>Ученики преподавателя · {students.length}</Overline>
              <Button variant="primary" size="sm" icon="add-student" onClick={() => setAddOpen(true)}>Добавить учеников</Button>
            </div>

            {addOpen && <StudentPickerModal candidates={assignable} onAssign={assignMany} onClose={() => setAddOpen(false)} />}

            {/* Статистика по ученикам */}
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 20 }}>
              <Card pad={20}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Icon name="trending-up" size={16} style={{ color: 'var(--green)' }} /><Overline>Самые активные</Overline></div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 12, maxHeight: 248, overflowY: 'auto', overflowX: 'hidden', paddingRight: 6 }}>
                  {frequent.length === 0 ? <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Нет данных за месяц.</div> : frequent.map((c, i) => (
                    <div key={c.id} className="row-hover" onClick={() => onOpenClient && onOpenClient(c)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 8px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', margin: '0 -8px' }}>
                      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 700, color: 'var(--text-dim)', width: 16, flex: 'none' }}>{i + 1}</span>
                      <Avatar animal={c.animal} tone={c.tone} size={28} />
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.child}</div>
                        <div style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>посл. занятие: {lastLessonLabel(studentStat(c).lastISO)}</div>
                      </div>
                      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: 'var(--green)', flex: 'none' }}>{studentStat(c).monthCount} зан.</span>
                    </div>
                  ))}
                </div>
              </Card>
              <Card pad={20}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Icon name="alert-triangle" size={16} style={{ color: 'var(--amber)' }} /><Overline>Давно не занимались</Overline></div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 12, maxHeight: 248, overflowY: 'auto', overflowX: 'hidden', paddingRight: 6 }}>
                  {inactive.length === 0 ? <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Все ученики занимались за последние 2 недели.</div> : inactive.map((c) => (
                    <div key={c.id} className="row-hover" onClick={() => onOpenClient && onOpenClient(c)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 8px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', margin: '0 -8px' }}>
                      <Avatar animal={c.animal} tone={c.tone} size={28} />
                      <span style={{ flex: 1, minWidth: 0, fontSize: 14, fontWeight: 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.child}</span>
                      <span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--amber-ink)', flex: 'none' }}>{lastLessonLabel(studentStat(c).lastISO)}</span>
                    </div>
                  ))}
                </div>
              </Card>
              <Card pad={20}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Icon name="history" size={16} style={{ color: 'var(--text-dim)' }} /><Overline>Занимались раньше</Overline></div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 12, maxHeight: 248, overflowY: 'auto', overflowX: 'hidden', paddingRight: 6 }}>
                  {formerStudents.length === 0 ? <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Бывших учеников нет.</div> : formerStudents.map((c) => (
                    <div key={c.id} className="row-hover" onClick={() => onOpenClient && onOpenClient(c)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 8px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', margin: '0 -8px' }}>
                      <Avatar animal={c.animal} tone={c.tone} size={28} />
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                          <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.child}</span>
                          {c.archived && <span style={{ fontSize: 10, fontWeight: 700, padding: '1px 5px', borderRadius: 3, background: 'var(--bg-soft)', color: 'var(--text-dim)', flex: 'none' }}>АРХИВ</span>}
                        </div>
                        <div style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>посл. занятие: {lastLessonLabel(studentStat(c).lastISO)}{c.teacher ? ' · сейчас: ' + c.teacher.split(' ')[0] : ''}</div>
                      </div>
                      <button onClick={e => { e.stopPropagation(); removeFromHistory(c); }} title="Удалить из истории педагога"
                        onMouseEnter={e => { e.currentTarget.style.background = 'var(--coral-bg)'; e.currentTarget.style.color = 'var(--coral)'; e.currentTarget.style.borderColor = '#E6C9C4'; }}
                        onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-dim)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
                        style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-dim)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none', transition: 'background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast)' }}>
                        <Icon name="trash-2" size={15} />
                      </button>
                    </div>
                  ))}
                </div>
              </Card>
            </div>

            {/* Таблица учеников */}
            <Card pad={0} style={{ overflow: 'hidden' }}>
              <div style={{ padding: '16px 16px 12px' }}><Overline>Все ученики</Overline></div>
              {students.length === 0 ? <div style={{ padding: '24px 16px', color: 'var(--text-dim)', fontSize: 14 }}>Учеников пока нет. Нажмите «Добавить ученика».</div> : (
                <div style={{ maxHeight: 420, overflowY: 'auto', overflowX: 'hidden' }}>
                <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                  <thead><tr style={{ position: 'sticky', top: 0, background: 'var(--bg-card)', zIndex: 1 }}>
                    <th style={th}>Ученик</th>
                    <th style={th}>Статус</th>
                    <th style={{ ...th, textAlign: 'center' }}>Занятий в этом мес.</th>
                    <th style={{ ...th, textAlign: 'right' }}>Баланс</th>
                    <th style={{ ...th, textAlign: 'right' }}>Оплата преп.</th>
                    <th style={th}>Посл. занятие</th>
                    <th style={{ ...th, width: 170 }}></th>
                  </tr></thead>
                  <tbody>
                    {students.map((c) => (
                      <tr key={c.id} className="row-hover" style={{ cursor: 'pointer' }} onClick={() => onOpenClient && onOpenClient(c)}>
                        <td style={{ ...td, fontWeight: 600 }}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                            <Avatar animal={c.animal} tone={c.tone} size={30} />{c.child}
                          </div>
                        </td>
                        <td style={td}><Pill kind={c.status} /></td>
                        <td style={{ ...td, textAlign: 'center', fontFamily: 'var(--font-mono)', color: studentStat(c).monthCount ? 'var(--text)' : 'var(--text-dim)' }}>{studentStat(c).monthCount}</td>
                        <td style={{ ...td, textAlign: 'right' }}><Balance value={c.balance || 0} /></td>
                        <td style={{ ...td, textAlign: 'right', fontFamily: 'var(--font-mono)', color: teacherPayNow ? 'var(--text)' : 'var(--text-dim)' }}>{teacherPayNow ? fmtNum(teacherPayNow) + ' ₽' : 'не задано'}</td>
                        <td style={{ ...td, color: 'var(--text-secondary)', fontSize: 13, whiteSpace: 'nowrap' }}>{lastLessonLabel(studentStat(c).lastISO)}</td>
                        <td style={td} onClick={e => e.stopPropagation()}>
                          <button onClick={() => unassignStudent(c)} title="Открепить ученика от преподавателя"
                            onMouseEnter={e => { e.currentTarget.style.background = 'var(--coral-bg)'; e.currentTarget.style.color = 'var(--coral)'; e.currentTarget.style.borderColor = '#E6C9C4'; }}
                            onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-secondary)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
                            style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', transition: 'background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast)' }}>
                            <Icon name="remove-record" size={16} />Открепить от учителя
                          </button>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                </div>
              )}
            </Card>
          </div>
        )}

        {/* ================= ВЫПЛАТЫ ================= */}
        {tab === 'payouts' && (
          <div style={{ maxWidth: 760 }}>
            <PayoutStats payouts={activePayouts} onPayout={() => onPayout && onPayout(staff)} />

            {payoutConflicts.length > 0 && (
              <div style={{ marginTop: 22, border: '1px solid #E6C9C4', borderRadius: 'var(--radius-md)', overflow: 'hidden' }}>
                <div style={{ padding: '12px 16px', background: 'var(--coral-bg)', display: 'flex', alignItems: 'center', gap: 8 }}>
                  <Icon name="alert-triangle" size={16} style={{ color: 'var(--coral)', flex: 'none' }} />
                  <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--coral)' }}>Конфликты выплат · {yearView}</span>
                  <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>— занятия, добавленные после выплаты</span>
                </div>
                <div style={{ display: 'flex', flexDirection: 'column' }}>
                  {payoutConflicts.map(c => (
                    <div key={c.mi} style={{ padding: '14px 16px', borderTop: '1px solid var(--border)' }}>
                      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, marginBottom: 10 }}>
                        <span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{SC_MONTHS_FULL[c.mi]} {c.year}</span>
                        <span style={{ fontSize: 12.5, color: 'var(--text-secondary)' }}>Выплачено <b style={{ fontFamily: 'var(--font-mono)' }}>{fmtNum(c.paid)} ₽</b> · к доплате <b style={{ fontFamily: 'var(--font-mono)', color: 'var(--coral)' }}>{fmtNum(c.unpaidSum)} ₽</b></span>
                      </div>
                      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
                        <div>
                          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--green-ink)', marginBottom: 6 }}>Оплачены ({c.paidLessons.length})</div>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 160, overflowY: 'auto' }}>
                            {c.paidLessons.length === 0 ? <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>—</span> : c.paidLessons.map(l => (
                              <div key={l.id} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
                                <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--green)', flex: 'none' }} />
                                <span style={{ flex: 1, minWidth: 0, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.dateLabel}{l.time ? ', ' + l.time : ''} · {l.who}</span>
                                <span style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-dim)' }}>{fmtNum(l.pay)} ₽</span>
                              </div>
                            ))}
                          </div>
                        </div>
                        <div>
                          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--coral)', marginBottom: 6 }}>Не оплачены ({c.unpaidLessons.length})</div>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 160, overflowY: 'auto' }}>
                            {c.unpaidLessons.map(l => (
                              <div key={l.id} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
                                <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--coral)', flex: 'none' }} />
                                <span style={{ flex: 1, minWidth: 0, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.dateLabel}{l.time ? ', ' + l.time : ''} · {l.who}</span>
                                <span style={{ fontFamily: 'var(--font-mono)', color: 'var(--coral)' }}>{fmtNum(l.pay)} ₽</span>
                              </div>
                            ))}
                          </div>
                        </div>
                      </div>
                      <button onClick={() => { const mm = String(c.mi + 1).padStart(2, '0'); const last = new Date(c.year, c.mi + 1, 0).getDate(); let end = `${c.year}-${mm}-${String(last).padStart(2, '0')}`; const tISO = window.APP_TODAY_ISO || end; if (end > tISO) end = tISO; onPayout && onPayout(staff, { period: { start: `${c.year}-${mm}-01`, end } }); }} style={{ marginTop: 12, display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 14px', borderRadius: 'var(--radius-sm)', border: 'none', background: 'var(--green)', color: '#fff', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600 }} onMouseEnter={e => { e.currentTarget.style.background = '#28593F'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--green)'; }}>
                        <Icon name="wallet" size={15} />Доплатить {fmtNum(c.unpaidSum)} ₽
                      </button>
                    </div>
                  ))}
                </div>
              </div>
            )}
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '26px 0 14px' }}>
              <Overline>История выплат · всего {fmtNum(paidTotal)} ₽</Overline>
              <Button variant="secondary" size="sm" icon="wallet" onClick={() => onPayout && onPayout(staff)}>Выплатить</Button>
            </div>
            {payouts.length === 0 ? <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Выплат пока не было.</div> : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {payouts.map(h => <PayoutCard key={h.id} h={h} students={students} staff={staff} onPay={setPayConfirm} onVoid={setVoidTarget} />)}
              </div>
            )}
          </div>
        )}
      </div>

      {/* Отказ по заявке: причина необязательна, но уходит преподавателю уведомлением. */}
      {rejectFor && (() => {
        const r = rejectFor;
        return (
          <div style={{ position: 'fixed', inset: 0, zIndex: 95, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
            <div onClick={() => setRejectFor(null)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.45)', backdropFilter: 'blur(2px)' }} />
            <div className="om-sheet-up" style={{ position: 'relative', width: 420, maxWidth: '100%', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', padding: 20 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
                <span style={{ width: 38, height: 38, borderRadius: 'var(--radius-md)', background: 'var(--coral-bg)', color: 'var(--coral)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}><Icon name="x" size={18} /></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <h3 style={{ fontSize: 16, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Отклонить заявку?</h3>
                  <div style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>{r.fieldLabel}: {r.oldValue || '—'} → {r.newValue}</div>
                </div>
              </div>
              <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5, marginTop: 12 }}>
                Значение не изменится. Причина уйдёт преподавателю в уведомлениях — по ней он поймёт, что исправить.
              </div>
              <input id="sc-reject-note" placeholder="Причина (необязательно)" autoFocus
                style={{ width: '100%', marginTop: 12, padding: '10px 12px', fontFamily: 'inherit', fontSize: 13.5, border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)', color: 'var(--text)', outline: 'none' }} />
              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
                <Button variant="ghost" onClick={() => setRejectFor(null)}>Назад</Button>
                <Button variant="danger" icon="x" disabled={reqBusy === r.id}
                  onClick={() => decideReq(r, false, (document.getElementById('sc-reject-note') || {}).value || '')}>Отклонить</Button>
              </div>
            </div>
          </div>
        );
      })()}

      {payConfirm && <PayConfirmModal h={payConfirm} onClose={() => setPayConfirm(null)} onConfirm={doPayNow} />}
      {voidTarget && <VoidModal h={voidTarget} onClose={() => setVoidTarget(null)} onConfirm={doVoid} />}

      {card && (
        <LessonCard
          context="staff"
          staff={staff}
          students={students}
          lesson={card.lesson}
          showTeacherMoney={true}
          onSave={saveLessonFromCard}
          onClose={() => setCard(null)}
        />
      )}

    </div>
  );
}

const navYrBtn = { width: 26, height: 26, borderRadius: 5, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', fontSize: 14, lineHeight: 1 };

// Deterministic lesson breakdown for a payout («за какие занятия рассчитана выплата»).
// Каждое занятие получает реальную дату внутри периода и оценивается ПО СТАВКЕ НА ЭТУ ДАТУ:
//  — для уже выплаченной (paid) выплаты берём замороженную ставку h.rate (исторический факт);
//  — для непроведённой — ставку из истории ставок преподавателя на дату занятия (rateOn),
//    поэтому смена ставки в середине периода корректно делит занятия по старой/новой ставке.
function genPayoutLessons(h, students, staff) {
  const n = h.lessons || 0;
  if (!n) return [];
  let seed = (h.id || 1) * 7919 + 17;
  const rnd = () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; };
  const names = (students && students.length) ? students.map(s => s.child) : ['Ученик'];
  const times = ['10:00', '12:00', '14:00', '16:00', '17:00', '18:00', '19:00'];
  const curM = (+(window.APP_TODAY_ISO || '2026-05-31').slice(5, 7)) - 1;
  const pm = parsePeriodMonth(h.period); const mIdx = pm < 0 ? curM : pm;
  const year = (parsePayDate(h.payDate) || {}).year || 2026;
  const daysInMonth = new Date(year, mIdx + 1, 0).getDate();
  const mm = String(mIdx + 1).padStart(2, '0');
  const paid = h.status === 'paid';
  const out = [];
  for (let i = 0; i < n; i++) {
    // равномерно распределяем занятия по дням месяца (свежие — позже)
    const day = Math.min(daysInMonth, Math.max(1, Math.round((i + 1) / n * daysInMonth)));
    const dateISO = `${year}-${mm}-${String(day).padStart(2, '0')}`;
    const amount = paid ? (h.rate || 0) : (staff && window.rateOn ? window.rateOn(staff, dateISO) : (h.rate || 0));
    out.push({ id: i, dateISO, dateLabel: `${day} ${SC_MONTHS[mIdx].toLowerCase()}`, time: times[Math.floor(rnd() * times.length)], who: names[Math.floor(rnd() * names.length)], amount });
  }
  out.sort((a, b) => (a.dateISO < b.dateISO ? 1 : -1));
  return out;
}
window.genPayoutLessons = genPayoutLessons;
// Сумма выплаты с дат-зависимыми ставками (для пересчёта непроведённых выплат).
function pricePayout(h, staff) {
  return genPayoutLessons(h, [], staff).reduce((a, l) => a + l.amount, 0);
}
window.pricePayout = pricePayout;

const PAY_MON_ABBR = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
function parsePayDate(s) {
  if (!s || s === '—') return null;
  const m = String(s).toLowerCase().match(/(\d{1,2})\s+([а-я]+)\s+(\d{4})/);
  if (!m) return null;
  const mi = PAY_MON_ABBR.findIndex(x => m[2].startsWith(x));
  if (mi < 0) return null;
  return { day: +m[1], month: mi, year: +m[3] };
}
// Месяц РАБОТЫ выплаты из её периода («1–30 апр» → 3). Нужно, чтобы помесячная таблица
// привязывала сформированную выплату к месяцу, за который она начислена.
function parsePeriodMonth(s) {
  if (!s) return null;
  // Берём ПОСЛЕДНЕЕ название месяца в строке периода (формат «1 мая – 31 мая 2026»
  // оканчивается годом, поэтому привязка только к концу строки не работала).
  const all = String(s).toLowerCase().match(/[а-я]{3,}/g);
  if (!all || !all.length) return null;
  for (let i = all.length - 1; i >= 0; i--) { const mi = PAY_MON_ABBR.findIndex(x => all[i].startsWith(x)); if (mi >= 0) return mi; }
  return null;
}

// Статистика по выплатам преподавателю — KPI, динамика по месяцам, статусы, способы, производные.
function PayoutStats({ payouts, onPayout }) {
  const years = Array.from(new Set(payouts.map(p => (parsePayDate(p.payDate) || {}).year).filter(Boolean)));
  const [year, setYear] = React.useState(years.includes(2026) ? 2026 : (years[0] || 2026));

  const paid = payouts.filter(p => p.status === 'paid');
  const planned = payouts.filter(p => p.status !== 'paid');
  const paidSum = paid.reduce((a, b) => a + b.amount, 0);
  const plannedSum = planned.reduce((a, b) => a + b.amount, 0);
  const paidLessons = paid.reduce((a, b) => a + (b.lessons || 0), 0);
  const avgPerLesson = paidLessons ? Math.round(paidSum / paidLessons) : 0;
  const avgPayout = paid.length ? Math.round(paidSum / paid.length) : 0;
  const paidDated = paid.map(p => ({ ...p, d: parsePayDate(p.payDate) })).filter(p => p.d);
  const monthsActive = new Set(paidDated.map(p => p.d.year + '-' + p.d.month)).size;
  const avgPerMonth = monthsActive ? Math.round(paidSum / monthsActive) : 0;
  const last = payouts.find(p => p.status === 'paid') || payouts[0];

  const monthSums = SC_MONTHS.map((label, mi) => paidDated.filter(p => p.d.year === year && p.d.month === mi).reduce((a, b) => a + b.amount, 0));
  const yearPaid = monthSums.reduce((a, b) => a + b, 0);

  // методы выплат (по оплаченным)
  const methodMap = {};
  paid.forEach(p => { const m = p.method || '—'; methodMap[m] = (methodMap[m] || 0) + p.amount; });
  const methods = Object.entries(methodMap).map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value);
  const methodColors = ['var(--brand)', 'var(--green)', 'var(--gold-ink)', 'var(--text-dim)'];

  const kpis = [
    { label: 'Всего выплачено', value: fmtNum(paidSum) + ' ₽', color: 'var(--green)', sub: paid.length + ' выплат' },
    { label: 'Ожидает выплаты', value: fmtNum(plannedSum) + ' ₽', color: plannedSum ? 'var(--amber)' : 'var(--text-dim)', sub: planned.length + ' запланировано' },
    { label: 'Среднее за месяц', value: fmtNum(avgPerMonth) + ' ₽', color: 'var(--text)', sub: monthsActive + ' мес. с выплатами' },
    { label: 'Последняя выплата', value: last ? fmtNum(last.amount) + ' ₽' : '—', color: 'var(--text)', sub: last ? (last.status === 'paid' ? last.payDate : 'запланирована') : '—' },
  ];

  const maxBar = Math.max(...monthSums, 1);
  const totalStatus = paidSum + plannedSum;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <Overline>Статистика по выплатам</Overline>

      {/* KPI */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
        {kpis.map((k, i) => (
          <Card key={i} pad={16}>
            <div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{k.label}</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 19, fontWeight: 700, marginTop: 6, color: k.color, whiteSpace: 'nowrap' }}>{k.value}</div>
            <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginTop: 3, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{k.sub}</div>
          </Card>
        ))}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16, alignItems: 'start' }}>
        {/* Динамика по месяцам */}
        <Card pad={20}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
            <Overline style={{ whiteSpace: 'nowrap' }}>Динамика выплат</Overline>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-dim)' }}>{fmtNum(yearPaid)} ₽</span>
              <Dropdown value={year} onChange={setYear} options={(years.length ? years : [2026]).sort((a, b) => b - a).map(y => ({ value: y, label: String(y) }))} width={92} />
            </div>
          </div>
          <div style={{ display: 'flex', alignItems: 'flex-end', gap: 5, height: 140, marginTop: 18 }}>
            {monthSums.map((v, i) => {
              const h = v > 0 ? Math.max((v / maxBar) * 112, 4) : 2;
              return (
                <div key={i} title={v ? `${SC_MONTHS[i]}: ${fmtNum(v)} ₽` : `${SC_MONTHS[i]}: нет выплат`} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
                  <div style={{ width: '100%', height: h, borderRadius: '4px 4px 0 0', background: v ? 'var(--brand-soft)' : 'var(--bg-soft)', transition: 'height 300ms var(--ease)' }} />
                  <span style={{ fontSize: 9.5, color: 'var(--text-dim)', fontWeight: 600 }}>{SC_MONTHS[i].slice(0, 1)}</span>
                </div>
              );
            })}
          </div>
        </Card>

        {/* Статусы + способы + производные */}
        <Card pad={20}>
          <Overline>Разбивка</Overline>
          <div style={{ marginTop: 14 }}>
            {[{ k: 'Выплачено', v: paidSum, c: 'var(--green)' }, { k: 'Запланировано', v: plannedSum, c: 'var(--amber)' }].map((s, i) => {
              const pct = totalStatus ? Math.round(s.v / totalStatus * 100) : 0;
              return (
                <div key={i} style={{ marginBottom: 10 }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, marginBottom: 5 }}>
                    <span style={{ color: 'var(--text-secondary)' }}>{s.k}</span>
                    <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{fmtNum(s.v)} ₽</span>
                  </div>
                  <div style={{ height: 8, borderRadius: 5, background: 'var(--bg-soft)', overflow: 'hidden' }}><div style={{ width: pct + '%', height: '100%', background: s.c, transition: 'width 300ms var(--ease)' }} /></div>
                </div>
              );
            })}
          </div>

          {methods.length > 0 && (
            <div style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--border)' }}>
              <div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700, marginBottom: 10 }}>Способы выплаты</div>
              <div style={{ display: 'flex', height: 8, borderRadius: 5, overflow: 'hidden', marginBottom: 10 }}>
                {methods.map((m, i) => <div key={i} title={`${m.label}: ${fmtNum(m.value)} ₽`} style={{ width: (m.value / paidSum * 100) + '%', background: methodColors[i % methodColors.length] }} />)}
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 12px' }}>
                {methods.map((m, i) => (
                  <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
                    <span style={{ width: 8, height: 8, borderRadius: 2, background: methodColors[i % methodColors.length] }} />{m.label}
                  </span>
                ))}
              </div>
            </div>
          )}

          <div style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--border)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <div>
              <div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700 }}>Средняя выплата</div>
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, marginTop: 3 }}>{fmtNum(avgPayout)} ₽</div>
            </div>
            <div>
              <div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700 }}>На занятие</div>
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, marginTop: 3 }}>{fmtNum(avgPerLesson)} ₽</div>
            </div>
          </div>
        </Card>
      </div>

      {/* К выплате сейчас */}
      {plannedSum > 0 && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px', borderRadius: 'var(--radius-md)', background: 'var(--gold-soft)', border: '1px solid #E7CFA3' }}>
          <Icon name="wallet" size={20} style={{ color: 'var(--gold-ink)', flex: 'none' }} />
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13.5, fontWeight: 700, color: 'var(--text)' }}>К выплате преподавателю: {fmtNum(plannedSum)} ₽</div>
            <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', marginTop: 1 }}>Запланированных начислений: {planned.length}</div>
          </div>
          <Button variant="primary" size="sm" icon="wallet" onClick={onPayout}>Выплатить</Button>
        </div>
      )}
    </div>
  );
}

const PAY_ISO_MON = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
const payFmtIso = (iso) => { const a = (iso || '').split('-'); return a.length === 3 ? `${+a[2]} ${PAY_ISO_MON[+a[1] - 1]} ${a[0]}` : (iso || '—'); };
const payIsoNow = () => window.APP_TODAY_ISO || '2026-05-31';

// Подтверждение оплаты запланированной выплаты (центр, om-sheet-up, ~420px).
// Дата оплаты (наш календарь) + способ (Dropdown). На время запроса кнопка — со спиннером/disabled.
function PayConfirmModal({ h, onClose, onConfirm }) {
  const [vis, setVis] = React.useState(false);
  const [payDate, setPayDate] = React.useState(payIsoNow());
  const [dateCal, setDateCal] = React.useState(false);
  const [method, setMethod] = React.useState(h.method && h.method !== '—' ? h.method : (window.CRM_DATA.PAYMENT_METHODS || [])[0] || 'СБП');
  const [busy, setBusy] = React.useState(false);
  const dateRef = React.useRef(null);
  const adj = h.adjustmentRubles || 0;
  React.useEffect(() => { const t = setTimeout(() => setVis(true), 10); return () => clearTimeout(t); }, []);
  React.useEffect(() => { const f = (e) => { if (dateRef.current && !dateRef.current.contains(e.target)) setDateCal(false); }; document.addEventListener('mousedown', f); return () => document.removeEventListener('mousedown', f); }, []);
  const dismiss = () => { if (busy) return; setVis(false); setTimeout(onClose, 190); };
  // Реальный запрос: ждём onConfirm (PATCH). Успех — родитель закроет модалку; ошибка — снимаем busy.
  const go = async () => { if (busy) return; setBusy(true); try { await onConfirm(h, { payDate, method }); } catch (e) { setBusy(false); } };
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 82, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div onClick={dismiss} style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.5)', backdropFilter: 'blur(2px)', opacity: vis ? 1 : 0, transition: 'opacity 190ms var(--ease)' }} />
      <div className={vis ? 'om-sheet-up' : ''} style={{ position: 'relative', width: 420, maxWidth: '100%', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', overflow: 'hidden', opacity: vis ? 1 : 0, transition: 'opacity 190ms var(--ease)' }}>
        <div style={{ padding: '18px 22px 14px', display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ width: 40, height: 40, borderRadius: 'var(--radius-md)', background: 'var(--green-bg)', color: 'var(--green-ink)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}><Icon name="wallet" size={20} /></span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <h3 style={{ fontSize: 17, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Выплатить зарплату?</h3>
            <div style={{ fontSize: 12.5, color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{h.teacher}</div>
          </div>
        </div>
        <div style={{ margin: '0 22px 14px', padding: '12px 14px', borderRadius: 'var(--radius-md)', background: 'var(--bg-soft)', border: '1px solid var(--border)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10 }}>
            <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Период</span>
            <span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--text)' }}>{h.period}</span>
          </div>
          {adj !== 0 && (
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10, marginTop: 6 }}>
              <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Корректировка</span>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: adj > 0 ? 'var(--green-ink)' : 'var(--coral)' }}>{adj > 0 ? '+' : '−'}{fmtNum(Math.abs(adj))} ₽</span>
            </div>
          )}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10, marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border)' }}>
            <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>К выплате</span>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 17, fontWeight: 700, color: 'var(--green)' }}>{fmtNum(h.amount)} ₽</span>
          </div>
        </div>
        <div style={{ padding: '0 22px 4px', display: 'flex', gap: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <label style={{ display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 5 }}>Дата оплаты</label>
            <div ref={dateRef} style={{ position: 'relative' }}>
              <button onClick={() => setDateCal(o => !o)} disabled={busy}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 8, width: '100%', justifyContent: 'flex-start', padding: '10px 12px', cursor: busy ? 'default' : 'pointer', background: 'var(--bg-card)', border: '1px solid ' + (dateCal ? 'var(--brand)' : 'var(--border)'), borderRadius: 'var(--radius-sm)', boxShadow: dateCal ? 'var(--shadow-focus)' : 'none', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 500, color: 'var(--text)' }}>
                <Icon name="calendar" size={15} style={{ color: 'var(--text-dim)' }} />{payFmtIso(payDate)}
              </button>
              {dateCal && <RangeCalendar single value={{ start: payDate, end: payDate }} maxISO={payIsoNow()} onApply={(r) => setPayDate(r.start)} onClose={() => setDateCal(false)} />}
            </div>
          </div>
          <div style={{ width: 150, flex: 'none' }}>
            <label style={{ display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 5 }}>Способ оплаты</label>
            <Dropdown value={method} onChange={setMethod} options={(window.CRM_DATA.PAYMENT_METHODS || []).map(m => ({ value: m, label: m }))} />
          </div>
        </div>
        <div style={{ padding: '16px 22px 18px', display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
          <Button variant="ghost" onClick={dismiss} style={busy ? { opacity: 0.5, cursor: 'not-allowed' } : {}}>Отмена</Button>
          <Button variant="primary" onClick={go} style={busy ? { opacity: 0.7, cursor: 'wait' } : {}}>
            {busy ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><span className="om-spin"><Icon name="loader" size={15} /></span>Выплата…</span> : 'Выплатить ' + fmtNum(h.amount) + ' ₽'}
          </Button>
        </div>
      </div>
    </div>
  );
}

// Сторно (отмена выплаты) — жёсткое предупреждение, действие необратимо.
function VoidModal({ h, onClose, onConfirm }) {
  const [vis, setVis] = React.useState(false);
  React.useEffect(() => { const t = setTimeout(() => setVis(true), 10); return () => clearTimeout(t); }, []);
  const dismiss = () => { setVis(false); setTimeout(onClose, 190); };
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 82, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div onClick={dismiss} style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.5)', backdropFilter: 'blur(2px)', opacity: vis ? 1 : 0, transition: 'opacity 190ms var(--ease)' }} />
      <div className={vis ? 'om-sheet-up' : ''} style={{ position: 'relative', width: 440, maxWidth: '100%', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', overflow: 'hidden', opacity: vis ? 1 : 0, transition: 'opacity 190ms var(--ease)' }}>
        <div style={{ padding: '18px 22px 14px', display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ width: 40, height: 40, borderRadius: 'var(--radius-md)', background: 'var(--coral-bg)', color: 'var(--coral)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}><Icon name="rotate-ccw" size={20} /></span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <h3 style={{ fontSize: 17, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Отменить выплату?</h3>
            <div style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>{h.period} · {fmtNum(h.amount)} ₽</div>
          </div>
        </div>
        <div style={{ margin: '0 22px 16px', padding: '12px 14px', borderRadius: 'var(--radius-md)', background: 'var(--gold-soft)', border: '1px solid #E7CFA3', display: 'flex', gap: 10 }}>
          <Icon name="alert-triangle" size={17} style={{ color: 'var(--gold-ink)', flex: 'none', marginTop: 1 }} />
          <span style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5 }}>Выплата будет отменена. Занятия из неё снова станут неоплаченными и попадут в следующий расчёт. Сумма исчезнет из статистики. <b style={{ color: 'var(--text)' }}>Действие необратимо.</b></span>
        </div>
        <div style={{ padding: '0 22px 18px', display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
          <Button variant="ghost" onClick={dismiss}>Назад</Button>
          <Button variant="danger" icon="rotate-ccw" onClick={() => onConfirm(h)}>Отменить выплату</Button>
        </div>
      </div>
    </div>
  );
}

function PayoutCard({ h, students, staff, onPay, onVoid }) {
  const [open, setOpen] = React.useState(false);
  const voided = !!h.voidedAt;
  const isPaid = h.status === 'paid';
  const adj = h.adjustmentRubles || 0;
  // Разбивка занятий: у свежесозданной выплаты есть реальный h.lessonsDetail; у загруженной из API —
  // тянем НАСТОЯЩИЙ снимок payout_lessons при первом раскрытии (не оценка). genPayoutLessons — только
  // фолбэк, если снимок не пришёл (ошибка/старое API).
  const [fetched, setFetched] = React.useState(null); // null = ещё не грузили
  const hasSnapshot = !!(h.lessonsDetail && h.lessonsDetail.length);
  React.useEffect(() => {
    if (!open || hasSnapshot || fetched != null || !window.fetchPayoutLessons) return;
    let alive = true;
    window.fetchPayoutLessons(h.id)
      .then(rows => { if (alive) setFetched(rows || []); })
      .catch(() => { if (alive) setFetched(genPayoutLessons(h, students, staff)); });
    return () => { alive = false; };
  }, [open, hasSnapshot, h.id]);
  const loadingLessons = open && !hasSnapshot && fetched == null;
  const lessons = hasSnapshot ? h.lessonsDetail : (fetched || []);
  const calc = lessons.reduce((a, b) => a + b.amount, 0);
  const rates = Array.from(new Set(lessons.map(l => l.amount)));
  const mixedRates = rates.length > 1; // в периоде сменилась ставка → занятия по разной цене
  return (
    <Card pad={0} style={{ overflow: 'hidden', opacity: voided ? 0.72 : 1, background: voided ? 'var(--bg-soft)' : 'var(--bg-card)' }}>
      <div onClick={() => setOpen(o => !o)} className="row-hover" style={{ padding: 14, cursor: 'pointer' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
          <span style={{ fontSize: 14, fontWeight: 600, color: voided ? 'var(--text-dim)' : 'var(--text)' }}>{h.period}</span>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
            {adj !== 0 && !voided && (
              <span title={h.adjustmentNote || (adj > 0 ? 'Премия' : 'Удержание')} style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5, fontWeight: 700, padding: '2px 7px', borderRadius: 4, background: adj > 0 ? 'var(--green-bg)' : 'var(--coral-bg)', color: adj > 0 ? 'var(--green-ink)' : 'var(--coral)' }}>{adj > 0 ? '+' : '−'}{fmtNum(Math.abs(adj))} ₽</span>
            )}
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, textDecoration: voided ? 'line-through' : 'none', color: voided ? 'var(--text-dim)' : isPaid ? 'var(--green)' : 'var(--amber)' }}>{fmtNum(h.amount)} ₽</span>
          </span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6, fontSize: 12, color: 'var(--text-dim)' }}>
          {voided
            ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, fontWeight: 600, color: 'var(--text-dim)' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--text-dim)' }} />Отменена</span>
            : <Pill kind={isPaid ? 'paid' : 'pending'} />}
          <span>·</span><span>{h.lessons} зан. {mixedRates ? '· ставки по дате' : (h.rate ? '× ' + fmtNum(h.rate) + ' ₽' : '')}</span>
          <span>·</span><span>{h.method}</span>
          <div style={{ flex: 1 }} />
          {voided
            ? <span style={{ fontFamily: 'var(--font-sans)' }}>сторнирована</span>
            : isPaid
            ? <span style={{ fontFamily: 'var(--font-mono)' }}>{h.payDate}</span>
            : <span style={{ fontFamily: 'var(--font-sans)', color: 'var(--amber-ink)', fontWeight: 600 }}>Запланирована</span>}
          <Icon name="chevron-down" size={16} style={{ color: 'var(--text-dim)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur) var(--ease)' }} />
        </div>
        {h.comment && <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', marginTop: 6 }}>{h.comment}</div>}
        {!voided && (onPay || onVoid) && (
          <div style={{ marginTop: 10, display: 'flex', gap: 8, alignItems: 'center' }}>
            {!isPaid && onPay && (
              <button onClick={e => { e.stopPropagation(); onPay(h); }}
                onMouseEnter={e => { e.currentTarget.style.background = '#28593F'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--green)'; }}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 14px', borderRadius: 'var(--radius-sm)', border: 'none', background: 'var(--green)', color: '#fff', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, transition: 'background var(--dur-fast) var(--ease)' }}>
                <Icon name="wallet" size={15} />Выплатить {fmtNum(h.amount)} ₽
              </button>
            )}
            <div style={{ flex: 1 }} />
            {onVoid && (
              <button onClick={e => { e.stopPropagation(); onVoid(h); }} title="Отменить выплату (сторно)"
                onMouseEnter={e => { e.currentTarget.style.background = 'var(--coral-bg)'; e.currentTarget.style.borderColor = '#E6C9C4'; e.currentTarget.style.color = 'var(--coral)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-dim)'; }}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', color: 'var(--text-dim)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, transition: 'all var(--dur-fast) var(--ease)' }}>
                <Icon name="rotate-ccw" size={14} />Отменить выплату
              </button>
            )}
          </div>
        )}
      </div>
      {open && (
        <div className="om-fade-in" style={{ borderTop: '1px solid var(--border)', background: 'var(--bg-soft)', padding: '10px 14px 12px' }}>
          <div style={{ fontSize: 11.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-dim)', marginBottom: 8 }}>Занятия в расчёте · {loadingLessons ? '…' : lessons.length}</div>
          {loadingLessons ? (
            <div style={{ fontSize: 13, color: 'var(--text-dim)', display: 'flex', alignItems: 'center', gap: 7, padding: '4px 0' }}><span className="om-spin"><Icon name="loader" size={14} /></span>Загружаем занятия…</div>
          ) : lessons.length === 0 ? (
            <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Занятий к оплате не было — выплата состоит только из корректировки.</div>
          ) : (
            <div style={{ maxHeight: 240, overflowY: 'auto', overflowX: 'hidden' }}>
              {lessons.map((l, i) => (
                <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderTop: i ? '1px solid var(--border)' : 'none' }}>
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--text-secondary)', width: 64, flex: 'none' }}>{l.dateLabel}</span>
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--text-dim)', width: 44, flex: 'none' }}>{l.time}</span>
                  <span style={{ flex: 1, minWidth: 0, fontSize: 13.5, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.who}</span>
                  {l.bonus > 0 ? (
                    <span title={l.bonusNote ? 'Надбавка: ' + l.bonusNote : 'Ставка + надбавка за это занятие'} style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, flex: 'none', display: 'inline-flex', alignItems: 'baseline', gap: 3 }}>
                      <span style={{ color: 'var(--text-secondary)', fontWeight: 600 }}>{fmtNum(l.rate != null ? l.rate : (l.amount - l.bonus))}</span>
                      <span style={{ color: 'var(--text-dim)' }}>+</span>
                      <span style={{ color: 'var(--gold-ink)', fontWeight: 700 }}>{fmtNum(l.bonus)}</span>
                      <span style={{ color: 'var(--text-dim)' }}>=</span>
                      <span style={{ color: 'var(--text)', fontWeight: 700 }}>{fmtNum(l.amount)} ₽</span>
                    </span>
                  ) : (
                    <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', flex: 'none' }}>{fmtNum(l.amount)} ₽</span>
                  )}
                </div>
              ))}
            </div>
          )}
          <div style={{ borderTop: '2px solid var(--border-strong)', marginTop: 6, paddingTop: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
            {adj !== 0 && !loadingLessons && (
              <React.Fragment>
                <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, color: 'var(--text-secondary)' }}>
                  <span>Расчёт по занятиям</span><span style={{ fontFamily: 'var(--font-mono)' }}>{fmtNum(calc)} ₽</span>
                </div>
                <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, fontSize: 12.5 }}>
                  <span style={{ color: adj > 0 ? 'var(--green-ink)' : 'var(--coral)', minWidth: 0 }}>Корректировка{h.adjustmentNote ? ' — ' + h.adjustmentNote : ''}</span>
                  <span style={{ fontFamily: 'var(--font-mono)', color: adj > 0 ? 'var(--green-ink)' : 'var(--coral)', flex: 'none', fontWeight: 600 }}>{adj > 0 ? '+' : '−'}{fmtNum(Math.abs(adj))} ₽</span>
                </div>
              </React.Fragment>
            )}
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
              <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{adj !== 0 ? 'К выплате' : 'Итого'}</span>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 700, color: 'var(--text)', textDecoration: voided ? 'line-through' : 'none' }}>{fmtNum(h.amount)} ₽</span>
            </div>
            {voided && <div style={{ fontSize: 11.5, color: 'var(--text-dim)', fontStyle: 'italic' }}>Выплата сторнирована — в статистику не входит.</div>}
          </div>
        </div>
      )}
    </Card>
  );
}

Object.assign(window, { StaffPage, StaffCard: StaffPage });
