/* global React, ReactDOM, Icon, Pill, DirChip, Avatar, Button, Overline, PILL, Dropdown, ModalLayer */
// Schools.ArtemenkoCRM — Client detail drawer (карточка клиента)

const CD_MONTHS = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
const cdRub = (n) => new Intl.NumberFormat('ru-RU').format(n) + ' ₽';
// «Будущее» занятие — по дате И ВРЕМЕНИ (сегодня, но позже «сейчас» = будущее → только «Запланировано»).
// Раньше сравнивали только дату → занятие сегодня-вечером можно было «провести» заранее.
// cdNormTime: «9:00»→«09:00» (иначе new Date(...'T9:00') = Invalid Date). Модульные — нужны и модалке
// (LessonHistoryPanel), и обработчику создания (ClientDetail.addLesson).
const cdNormTime = (t) => { const s = String(t || '').trim(); const m = s.match(/^(\d{1,2})(?::(\d{1,2}))?$/); return m ? String(m[1]).padStart(2, '0') + ':' + String(m[2] || '0').padStart(2, '0') : s; };
const isFutureDT = (iso, t) => { const nt = cdNormTime(t); return new Date((iso || '') + 'T' + (/^\d{2}:\d{2}$/.test(nt) ? nt : '23:59') + ':00').getTime() > Date.now(); };

// Мультифильтр: кнопка как у Dropdown, но в меню — чекбоксы (можно выбрать несколько).
// selected — массив значений; пустой = «все». onChange отдаёт новый массив.
function MultiFilter({ options, selected, onChange, allLabel, width }) {
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const ref = React.useRef(null);
  const menuRef = React.useRef(null);
  const sel = selected || [];
  React.useEffect(() => {
    if (!open) return;
    if (ref.current) setRect(ref.current.getBoundingClientRect());
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && menuRef.current && !menuRef.current.contains(e.target)) setOpen(false); };
    const onScroll = () => setOpen(false);
    document.addEventListener('mousedown', onDoc);
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => { document.removeEventListener('mousedown', onDoc); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onScroll); };
  }, [open]);

  const toggle = (v) => { onChange(sel.includes(v) ? sel.filter(x => x !== v) : [...sel, v]); };
  const isDefault = sel.length === 0;
  const label = isDefault ? allLabel
    : sel.length === 1 ? (options.find(o => o.value === sel[0]) || {}).label || allLabel
      : `Выбрано: ${sel.length}`;

  const menuW = rect ? Math.max(rect.width, 200) : 200;
  const belowRoom = rect ? window.innerHeight - rect.bottom : 999;
  const flipUp = belowRoom < 280 && rect && rect.top > belowRoom;
  const left = rect ? Math.min(rect.left, window.innerWidth - menuW - 8) : 0;

  return (
    <div ref={ref} style={{ position: 'relative', width }}>
      <button type="button" onClick={() => setOpen(o => !o)}
        onMouseEnter={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border-strong)'; e.currentTarget.style.background = 'var(--bg-soft)'; }}
        onMouseLeave={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg-card)'; }}
        style={{
          display: 'inline-flex', alignItems: 'center', gap: 8, width: width ? '100%' : 'auto',
          justifyContent: 'space-between', padding: '8px 12px', cursor: 'pointer',
          background: 'var(--bg-card)', border: '1px solid ' + (open ? 'var(--brand)' : 'var(--border)'),
          borderRadius: 'var(--radius-sm)', boxShadow: open ? 'var(--shadow-focus)' : 'none',
          fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 500,
          color: isDefault ? 'var(--text-secondary)' : 'var(--text)', whiteSpace: 'nowrap',
          transition: 'border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease)',
        }}>
        <span style={{ flex: 1, minWidth: 0, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', gap: 6 }}>
          {label}
          {sel.length > 1 && <span style={{ flex: 'none', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: 'var(--brand-ink)', background: 'var(--brand-soft)', borderRadius: 999, padding: '1px 7px' }}>{sel.length}</span>}
        </span>
        <Icon name="chevron-down" size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />
      </button>
      {open && rect && ReactDOM.createPortal(
        <div ref={menuRef} className="om-fade-in" style={{
          position: 'fixed', left, width: menuW, zIndex: 9999,
          top: flipUp ? undefined : rect.bottom + 4, bottom: flipUp ? (window.innerHeight - rect.top + 4) : undefined,
          background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
          boxShadow: 'var(--shadow-lg)', padding: 4, display: 'flex', flexDirection: 'column', maxHeight: 320,
        }}>
          <button type="button" onClick={() => onChange([])} style={{
            display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left', padding: '8px 10px',
            border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-sm)', background: 'transparent',
            color: isDefault ? 'var(--brand-ink)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: isDefault ? 600 : 500, whiteSpace: 'nowrap',
          }}
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-soft)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
            <span style={{ flex: 1 }}>{allLabel}</span>
            {isDefault && <Icon name="check" size={15} strokeWidth={2.5} />}
          </button>
          <div style={{ height: 1, background: 'var(--border)', margin: '4px 6px' }} />
          <div style={{ overflowY: 'auto', flex: 1 }}>
            {options.map(o => {
              const on = sel.includes(o.value);
              return (
                <button key={String(o.value)} type="button" onClick={() => toggle(o.value)} style={{
                  display: 'flex', alignItems: 'center', gap: 9, width: '100%', textAlign: 'left',
                  padding: '8px 10px', border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-sm)',
                  background: on ? 'var(--brand-soft)' : 'transparent', color: on ? 'var(--brand-ink)' : 'var(--text)',
                  fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: on ? 600 : 500, whiteSpace: 'nowrap',
                }}
                onMouseEnter={e => { if (!on) e.currentTarget.style.background = 'var(--bg-soft)'; }}
                onMouseLeave={e => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
                  <span style={{ width: 17, height: 17, flex: 'none', borderRadius: 5, border: '1.5px solid ' + (on ? 'var(--brand)' : 'var(--border-strong)'), background: on ? 'var(--brand)' : 'transparent', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
                    {on && <Icon name="check" size={12} strokeWidth={3} />}
                  </span>
                  <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{o.label}</span>
                </button>
              );
            })}
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

// Сократить ФИО педагога до «Фамилия И.О.»
function shortTeacher(name) {
  if (!name) return '—';
  const p = String(name).trim().split(/\s+/);
  return p[0] + (p[1] ? ' ' + p[1][0] + '.' : '') + (p[2] ? p[2][0] + '.' : '');
}

// Deterministic lesson history for a client (mirror of the teacher's conducted lessons).
// teachers — список педагогов, у которых числится ученик; каждое занятие привязывается к одному из них.
// Цена занятия для ученика ЗАВИСИТ ОТ ПЕДАГОГА (вариант C, ур. «ученик × педагог»).
// teacherPrices: { имяПедагога: цена }. Откат — общий client.lessonPrice (легаси/дефолт).
// Единое окно цены «ученик × педагог». Если у пары НЕ проставлена своя цена — это сбой:
// сигналим (console.warn + реестр window.__missingPrices), чтобы UI мог подсветить «назначьте
// цену срочно», но возвращаем дефолт, чтобы расчёты не падали.
window.__missingPrices = window.__missingPrices || {};
function priceFor(client, teacherName) {
  const m = client && client.teacherPrices;
  if (m && teacherName != null && m[teacherName] != null) return m[teacherName];
  if (teacherName) {
    const key = (client && client.id) + ' × ' + teacherName;
    if (!window.__missingPrices[key]) {
      window.__missingPrices[key] = { clientId: client && client.id, child: client && client.child, teacher: teacherName };
      console.warn('[priceFor] НЕ назначена цена занятия для пары «' + (client && client.child) + ' × ' + teacherName + '» — срочно задайте цену в карточке клиента. Использован дефолт ' + (window.DEFAULT_LESSON_PRICE || 1800) + ' ₽.');
    }
  }
  return window.DEFAULT_LESSON_PRICE || 1800;
}
// Есть ли у пары собственная цена (для UI-сигнала «цена не назначена»).
function hasPriceFor(client, teacherName) {
  const m = client && client.teacherPrices;
  return !!(m && teacherName != null && m[teacherName] != null);
}
window.hasPriceFor = hasPriceFor;
window.priceFor = priceFor;

function genClientLessons(client, teachers) {
  let seed = (client.id || 1) * 2654435761 % 2147483647;
  const rnd = () => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; };
  let cur = client.lastLesson ? new Date(client.lastLesson + 'T00:00:00') : new Date(2026, 4, 31);
  const base = client.lessonsMonth != null ? client.lessonsMonth : 4;
  const count = Math.max(8, base * 3 + Math.floor(rnd() * 8));
  const times = ['10:00', '12:00', '14:00', '16:00', '17:00', '18:00', '19:00'];
  const tList = (teachers && teachers.length) ? teachers : ['Не назначен'];
  const out = [];
  const todayISO = window.APP_TODAY_ISO || '2026-05-31';
  for (let i = 0; i < count; i++) {
    const r = rnd();
    // kind — стабильный «тип» занятия (не зависит от баланса):
    //  normal — обычное проведённое; cancelled — отменено; noshow — «не пришёл» (отдельная метка).
    // «Запланировано» определяется ДАТОЙ в будущем (не отдельным kind), поэтому планируемое
    // занятие получает реальную будущую дату — иначе прошлое занятие ошибочно станет «Запланировано».
    let kind = 'normal';
    let planned = false;
    if (i === 0 && client.status === 'active' && rnd() < 0.5) planned = true;
    else if (r < 0.10) kind = 'cancelled';
    else if (r < 0.20) kind = 'noshow';
    let dateObj = cur;
    if (planned) { const f = new Date(todayISO + 'T00:00:00'); f.setDate(f.getDate() + 2 + Math.floor(rnd() * 6)); dateObj = f; }
    const y = dateObj.getFullYear(), m = dateObj.getMonth(), d = dateObj.getDate();
    const teacher = tList.length === 1 ? tList[0]
      : (i < 4 || rnd() < 0.55) ? tList[0] : tList[1 + Math.floor(rnd() * (tList.length - 1))];
    out.push({
      id: client.id * 1000 + i,
      dateISO: `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`,
      dateLabel: `${d} ${CD_MONTHS[m]} ${y}`,
      time: times[Math.floor(rnd() * times.length)],
      kind, penalty: 0, amount: priceFor(client, teacher), teacher, status: 'paid',
    });
    cur = new Date(cur); cur.setDate(cur.getDate() - (2 + Math.floor(rnd() * 4)));
  }
  applyLedger(client, out);
  return out;
}

// Леджер ученика: баланс — единый кошелёк. Статус «Оплачено/В долг» каждого проведённого
// (normal, в прошлом) занятия выводится из баланса: идём от старых к новым, пока суммарная
// стоимость укладывается в доступные средства — «Оплачено», дальше — «В долг». Пополнение
// автоматически гасит самые старые долги. Отменённое занятие возврат суммы (не списывается).
// «Не пришёл» — отдельная метка; вместо стоимости занятия с баланса берётся штраф (penalty).
function applyLedger(client, lessons) {
  if (!Array.isArray(lessons)) return [];
  const balance = client.balance || 0;
  // «Запланировано» = ФАКТ занятия ещё scheduled (kind='future'), КАК В ДВИЖКЕ — не по дате.
  // Раньше по дате (dateISO > today): сегодняшнее/прошедшее scheduled (напр. при отставшем cron)
  // красилось paid/debt и сдвигало FIFO — расходилось с серверным payment. Теперь совпадает.
  const isFuture = (l) => l.kind === 'future';
  // Статусы по ФАКТУ занятия (не по прежнему status — иначе смена типа не пересчитается).
  lessons.forEach(l => {
    if (isFuture(l)) { l.status = 'planned'; return; }
    if (l.kind === 'cancelled') l.status = 'cancelled';
    else if (l.kind === 'noshow') l.status = 'noshow';
    else l.status = 'paid'; // нормальное прошедшее — уточним paid/debt ниже
  });
  const normalPast = lessons.filter(l => !isFuture(l) && l.kind === 'normal')
    .sort((a, b) => (a.dateISO < b.dateISO ? -1 : a.dateISO > b.dateISO ? 1 : 0));
  const totalCost = normalPast.reduce((s, l) => s + l.amount, 0);
  const availForLessons = balance + totalCost; // = деньги_внесённые − штрафы (т.к. balance уже их учёл)
  let cum = 0;
  normalPast.forEach(l => { cum += l.amount; l.status = cum <= availForLessons + 0.001 ? 'paid' : 'debt'; });
  return lessons;
}
window.applyLedger = applyLedger;

// История занятий ученика генерируется ОДИН раз и замораживается на записи (client.__lessons).
// Это критично: педагог у каждого занятия фиксируется навсегда. Смена/добавление педагога
// ученику НЕ перепривязывает прошлые занятия — иначе новый педагог «наследует» чужую историю.
function ensureClientLessons(client) {
  if (!client) return [];
  if (Array.isArray(client.__lessons)) return client.__lessons;
  // Новый клиент (создан вручную/из заявки) — НИКАКОЙ синтетической истории: он только что появился,
  // занятия начнут накапливаться вперёд из расписания. Иначе назначенному педагогу падают фантомные занятия.
  if (client.noHistory) {
    Object.defineProperty(client, '__lessons', { value: [], enumerable: false, writable: true, configurable: true });
    return client.__lessons;
  }
  // Реальные занятия грузятся из API (эффект reloadLessons в ClientDetail).
  // Синтетическую историю больше НЕ генерим — только то, что есть в БД.
  Object.defineProperty(client, '__lessons', { value: [], enumerable: false, writable: true, configurable: true });
  return client.__lessons;
}

function LessonHistoryPanel({ client, ledgerV, ledgerLog, onCancelLesson, onSetPenalty, onDeleteLesson, onChangeType, onAddLesson, onSaveLesson, onPreview }) {
  const LS = (window.CRM_DATA && window.CRM_DATA.LESSON_STATUS) || {};
  const all = React.useMemo(() => ensureClientLessons(client), [client.id, ledgerV]);
  const [penaltyFor, setPenaltyFor] = React.useState(null); // занятие, для которого вводим штраф
  const [penaltyVal, setPenaltyVal] = React.useState('');
  const [delFor, setDelFor] = React.useState(null); // занятие, удаление которого подтверждаем
  const [menuFor, setMenuFor] = React.useState(null); // занятие, для которого открыто меню смены типа
  const [confirmFor, setConfirmFor] = React.useState(null); // { lesson, type, preview } — подтверждение смены типа
  const [card, setCard] = React.useState(null); // {lesson} = «Карточка занятия» открыта (lesson=null → создание); null = закрыта
  const [showLog, setShowLog] = React.useState(false);
  const todayISO = window.APP_TODAY_ISO || '2026-05-31';
  // Педагоги для фильтра: те, кто реально есть в истории, + текущий (даже если занятий ещё нет).
  const teachers = React.useMemo(() => {
    const set = new Set(all.map(l => l.teacher).filter(Boolean));
    [client.teacher, ...(client.teachers || [])].filter(Boolean).forEach(t => set.add(t));
    return Array.from(set);
  }, [client.id]);
  const [statusF, setStatusF] = React.useState([]); // мультивыбор статусов; [] = все
  const [teacherF, setTeacherF] = React.useState([]); // мультивыбор педагогов; [] = все
  const [sort, setSort] = React.useState('new');
  const rows = all.filter(l => (statusF.length === 0 || statusF.includes(l.status)) && (teacherF.length === 0 || teacherF.includes(l.teacher))).slice()
    .sort((a, b) => { const k = (a.dateISO || '') + ' ' + (a.time || ''); const j = (b.dateISO || '') + ' ' + (b.time || ''); return sort === 'new' ? (k < j ? 1 : k > j ? -1 : 0) : (k > j ? 1 : k < j ? -1 : 0); });
  const statusOpts = ['paid', 'debt', 'planned', 'noshow', 'cancelled'].map(k => ({ value: k, label: (LS[k] || {}).label }));
  const teacherOpts = teachers.map(t => ({ value: t, label: shortTeacher(t) }));
  const sortOpts = [{ value: 'new', label: 'Сначала новые' }, { value: 'old', label: 'Сначала старые' }];
  const conducted = all.filter(l => l.status === 'paid' || l.status === 'debt' || l.status === 'noshow').length;
  const paidSum = all.filter(l => l.status === 'paid').reduce((a, b) => a + b.amount, 0);
  const debtSum = all.filter(l => l.status === 'debt').reduce((a, b) => a + b.amount, 0);
  return (
    <React.Fragment>
      <div style={{ padding: '18px 20px 12px', borderBottom: '1px solid var(--border)', flex: 'none' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <Overline style={{ whiteSpace: 'nowrap' }}>История занятий</Overline>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-dim)' }}>{all.length}</span>
            {onSaveLesson && (
              <button onClick={() => setCard({ lesson: null })}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 5, border: '1px solid var(--border)', background: 'var(--bg-card)', borderRadius: 'var(--radius-sm)', padding: '5px 10px', fontSize: 12.5, fontWeight: 600, color: 'var(--brand-ink)', cursor: 'pointer', fontFamily: 'var(--font-sans)' }}
                onMouseEnter={e => { e.currentTarget.style.background = 'var(--brand-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; }}>
                <Icon name="plus" size={14} />Занятие
              </button>
            )}
          </div>
        </div>
        <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 4 }}>Проведено <b style={{ color: 'var(--text)', fontFamily: 'var(--font-mono)' }}>{conducted}</b> · оплачено <b style={{ color: 'var(--green)', fontFamily: 'var(--font-mono)' }}>{cdRub(paidSum)}</b>{debtSum > 0 && <React.Fragment> · в долг <b style={{ color: 'var(--coral)', fontFamily: 'var(--font-mono)' }}>{cdRub(debtSum)}</b></React.Fragment>}</div>
        <div style={{ display: 'flex', gap: 6, marginTop: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}><MultiFilter selected={teacherF} onChange={setTeacherF} options={teacherOpts} allLabel="Все педагоги" width="100%" /></div>
          <div style={{ flex: 1, minWidth: 0 }}><MultiFilter selected={statusF} onChange={setStatusF} options={statusOpts} allLabel="Все статусы" width="100%" /></div>
          <div style={{ flex: 1, minWidth: 0 }}><Dropdown value={sort} onChange={setSort} options={sortOpts} placeholder="Сортировка" width="100%" /></div>
        </div>
      </div>
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', padding: '4px 20px 18px' }}>
        {rows.length === 0 ? (
          <div style={{ fontSize: 13, color: 'var(--text-dim)', padding: '20px 0' }}>По фильтру занятий нет.</div>
        ) : rows.map((l, i) => {
          const s = LS[l.status] || {};
          const actionable = l.status === 'paid' || l.status === 'debt';
          const future = l.dateISO > todayISO;
          // Тип занятия — со своим цветом (НЕ зелёный/красный: они закреплены за оплатой).
          const ti = future ? { label: 'Запланировано', icon: 'calendar-clock', fg: '#6d3bd4', soft: 'rgba(124,58,237,0.10)', bd: 'rgba(124,58,237,0.32)' }
            : l.kind === 'noshow' ? { label: 'Не пришёл', icon: 'user-x', fg: '#9a6a00', soft: 'rgba(212,150,18,0.12)', bd: 'rgba(212,150,18,0.36)' }
            : l.kind === 'cancelled' ? { label: 'Отменено', icon: 'x', fg: '#5b6472', soft: 'rgba(91,100,114,0.10)', bd: 'rgba(91,100,114,0.30)' }
            : { label: 'Проведено', icon: 'check', fg: '#2563c9', soft: 'rgba(37,99,201,0.10)', bd: 'rgba(37,99,201,0.32)' };
          // Кружок оплаты: зелёный — оплачено, красный — в долг, серый — оплата неприменима.
          const payColor = l.status === 'paid' ? 'var(--green)' : l.status === 'debt' ? 'var(--coral)' : 'var(--border-strong)';
          const payText = l.status === 'paid' ? 'Оплачено' : l.status === 'debt' ? 'В долг' : ti.label;
          const typeItems = [
            { type: 'normal', label: 'Проведено', disabled: future, hint: 'Будущее занятие нельзя отметить проведённым — внесите деньги на баланс' },
            { type: 'noshow', label: 'Не пришёл', disabled: future, hint: 'Доступно только для прошедших занятий' },
            { type: 'cancelled', label: 'Отменено', disabled: false },
            { type: 'future', label: 'Запланировано', disabled: !future, hint: 'Прошедшее занятие нельзя сделать запланированным — оно станет «Оплачено» или «В долг»' },
          ];
          return (
            <div key={l.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 0', borderTop: i ? '1px solid var(--border)' : 'none', position: 'relative' }}>
              <span title={payText} style={{ width: 8, height: 8, borderRadius: '50%', background: payColor, flex: 'none' }} />
              <div onClick={() => onSaveLesson && setCard({ lesson: l })} title="Открыть карточку занятия" style={{ flex: 1, minWidth: 0, cursor: onSaveLesson ? 'pointer' : 'default' }}>
                <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--text)' }}>{l.dateLabel}</div>
                <div style={{ fontSize: 11.5, color: 'var(--text-dim)' }}><span style={{ fontFamily: 'var(--font-mono)' }}>{l.time}</span> · {payText}</div>
              </div>
              {onChangeType && (
                <button title="Изменить статус занятия" onClick={() => setMenuFor(menuFor === l.id ? null : l.id)}
                  style={{ border: '1px solid ' + ti.bd, background: menuFor === l.id ? ti.soft : 'var(--bg-card)', borderRadius: 6, padding: '4px 9px', fontSize: 11.5, fontWeight: 600, color: ti.fg, cursor: 'pointer', fontFamily: 'var(--font-sans)', display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none', width: 138, boxSizing: 'border-box' }}
                  onMouseEnter={e => { if (menuFor !== l.id) e.currentTarget.style.background = ti.soft; }} onMouseLeave={e => { if (menuFor !== l.id) e.currentTarget.style.background = 'var(--bg-card)'; }}>
                  <Icon name={ti.icon} size={14} style={{ flex: 'none', color: ti.fg }} /><span style={{ flex: 1, textAlign: 'left' }}>{ti.label}</span><Icon name="chevron-down" size={13} style={{ flex: 'none', transform: menuFor === l.id ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur) var(--ease)' }} />
                </button>
              )}
              <span title={l.teacher} style={{ fontSize: 12, color: 'var(--text-secondary)', flex: 'none', width: 116, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'right' }}>{shortTeacher(l.teacher)}</span>
              {l.status === 'noshow' ? (
                <span style={{ flex: 'none', minWidth: 86, textAlign: 'right', lineHeight: 1.2 }}>
                  <span style={{ display: 'block', fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: l.penalty ? 'var(--amber-ink)' : 'var(--text-dim)' }}>{l.penalty ? cdRub(l.penalty) : '—'}</span>
                  <span style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Штраф</span>
                </span>
              ) : (
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, flex: 'none', minWidth: 86, textAlign: 'right', color: l.status === 'paid' ? 'var(--green)' : l.status === 'debt' ? 'var(--coral)' : l.status === 'cancelled' ? 'var(--text-dim)' : 'var(--text-secondary)', textDecoration: l.status === 'cancelled' ? 'line-through' : 'none' }}>{cdRub(l.amount)}</span>
              )}
              {onDeleteLesson && (
                <button title="Удалить занятие из истории" onClick={() => setDelFor(l)}
                  style={{ border: '1px solid var(--border)', background: 'var(--bg-card)', borderRadius: 6, width: 26, height: 26, flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-dim)', cursor: 'pointer' }}
                  onMouseEnter={e => { e.currentTarget.style.color = 'var(--coral)'; e.currentTarget.style.borderColor = '#E6C9C4'; e.currentTarget.style.background = 'var(--coral-bg)'; }} onMouseLeave={e => { e.currentTarget.style.color = 'var(--text-dim)'; e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg-card)'; }}>
                  <Icon name="trash-2" size={14} />
                </button>
              )}
              {menuFor === l.id && (
                <React.Fragment>
                  <div onClick={() => setMenuFor(null)} style={{ position: 'fixed', inset: 0, zIndex: 8 }} />
                  <div className="om-fade-in" style={{ position: 'absolute', top: 'calc(100% - 4px)', right: 92, zIndex: 9, width: 210, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', padding: 5 }}>
                    <div style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '6px 8px 4px' }}>Тип занятия</div>
                    {typeItems.map(it => (
                      <button key={it.type} title={it.disabled ? it.hint : ''} onClick={() => {
                        if (it.disabled) { onChangeType(l, it.type); return; } // покажет пояснение-тост
                        if (it.type === 'noshow') { setMenuFor(null); setPenaltyFor(l); setPenaltyVal(String(l.penalty || priceFor(client, l.teacher) || '')); return; }
                        if (it.type === l.kind) { setMenuFor(null); return; } // тип не изменился
                        setMenuFor(null); setConfirmFor({ lesson: l, type: it.type, preview: onPreview(l, it.type) }); // подтверждение с предпросмотром
                      }} style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left', border: 'none', background: 'transparent', borderRadius: 'var(--radius-sm)', padding: '8px 8px', cursor: it.disabled ? 'not-allowed' : 'pointer', opacity: it.disabled ? 0.4 : 1, fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500, color: 'var(--text)' }}
                        onMouseEnter={e => { if (!it.disabled) e.currentTarget.style.background = 'var(--bg-soft)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
                        <Icon name={it.type === 'normal' ? 'check' : it.type === 'noshow' ? 'user-x' : it.type === 'cancelled' ? 'x' : 'calendar-clock'} size={14} style={{ flex: 'none', color: 'var(--text-dim)' }} />
                        {it.label}{it.disabled && <Icon name="lock" size={11} style={{ marginLeft: 'auto', color: 'var(--text-dim)' }} />}
                      </button>
                    ))}
                    <div style={{ fontSize: 10, color: 'var(--text-dim)', padding: '4px 8px 6px', lineHeight: 1.4 }}>«Оплачено» / «В долг» система ставит сама по балансу.</div>
                  </div>
                </React.Fragment>
              )}
            </div>
          );
        })}
        {ledgerLog && ledgerLog.length > 0 && (
          <div style={{ marginTop: 14, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
            <button onClick={() => setShowLog(v => !v)} style={{ display: 'flex', alignItems: 'center', gap: 6, border: 'none', background: 'none', cursor: 'pointer', padding: 0, fontFamily: 'var(--font-sans)', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-dim)' }}>
              <Icon name="receipt" size={13} />Журнал баланса · {ledgerLog.length}
              <Icon name="chevron-down" size={13} style={{ transform: showLog ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur) var(--ease)' }} />
            </button>
            {showLog && (
              <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
                {ledgerLog.slice(0, 40).map(e => (
                  <div key={e.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }}>
                    <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-dim)', width: 84, flex: 'none' }}>{e.when}</span>
                    <span style={{ flex: 1, minWidth: 0, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.note}{e.lessonLabel ? ' · ' + e.lessonLabel : ''}</span>
                    <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, flex: 'none', color: e.type === 'topup' || e.type === 'refund' || e.type === 'clear' ? 'var(--green)' : 'var(--coral)' }}>{e.type === 'topup' || e.type === 'refund' ? '+' : e.type === 'clear' ? '' : '−'}{cdRub(e.amount)}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
      {card && (
        <LessonCard
          context="client"
          client={client}
          teachers={teachers}
          lesson={card.lesson}
          showTeacherMoney={true}
          onSave={onSaveLesson}
          onClose={() => setCard(null)}
        />
      )}
      {penaltyFor && (
        <ModalLayer z={95}>
          <div onClick={() => setPenaltyFor(null)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.4)' }} />
          <div className="om-sheet-up" style={{ position: 'relative', width: 320, background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', padding: 20 }}>
            <h3 style={{ fontSize: 16, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Штраф за неявку</h3>
            <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 2 }}>{penaltyFor.dateLabel} · списывается с баланса</div>
            <input autoFocus value={penaltyVal} onChange={e => setPenaltyVal(e.target.value.replace(/\D/g, ''))} placeholder="0 — без штрафа" style={{ width: '100%', boxSizing: 'border-box', marginTop: 14, border: '1px solid var(--brand)', boxShadow: 'var(--shadow-focus)', borderRadius: 'var(--radius-sm)', padding: '10px 12px', fontFamily: 'var(--font-mono)', fontSize: 14, color: 'var(--text)', outline: 'none' }} />
            {(() => {
              const pv = onPreview(penaltyFor, 'noshow', parseInt(penaltyVal || '0', 10) || 0);
              return (
                <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {pv.delta !== 0 && (
                    <div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.45 }}>
                      {pv.delta < 0 ? 'Спишется ' : 'Вернётся '}<b style={{ fontFamily: 'var(--font-mono)', color: 'var(--text)' }}>{cdRub(Math.abs(pv.delta))}</b> · баланс <span style={{ fontFamily: 'var(--font-mono)' }}>{cdRub(pv.balanceBefore)}</span> → <b style={{ fontFamily: 'var(--font-mono)', color: pv.balanceAfter < 0 ? 'var(--coral)' : 'var(--text)' }}>{cdRub(pv.balanceAfter)}</b>
                    </div>
                  )}
                  {pv.flips.length > 0 && (
                    <div style={{ fontSize: 11.5, color: 'var(--gold-ink)', display: 'flex', gap: 6, alignItems: 'flex-start' }}>
                      <Icon name="alert-triangle" size={13} style={{ flex: 'none', marginTop: 1 }} />
                      <span>Пересчитаются {pv.flips.length} занят.: {pv.flips.slice(0, 2).map(f => f.label + ' → ' + (LS[f.to] || {}).label).join(', ')}{pv.flips.length > 2 ? '…' : ''}</span>
                    </div>
                  )}
                </div>
              );
            })()}
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
              <Button variant="ghost" onClick={() => setPenaltyFor(null)}>Отмена</Button>
              <Button variant="primary" icon="check" onClick={() => { onSetPenalty(penaltyFor, parseInt(penaltyVal || '0', 10) || 0); setPenaltyFor(null); }}>Применить</Button>
            </div>
          </div>
        </ModalLayer>
      )}
      {confirmFor && (() => {
        const p = confirmFor.preview; const tType = confirmFor.type;
        const typeLabel = { normal: 'Проведено', cancelled: 'Отменено', future: 'Запланировано', noshow: 'Не пришёл' }[tType] || tType;
        const tgtTo = LS[p.targetTo] || {};
        return (
          <ModalLayer z={95}>
            <div onClick={() => setConfirmFor(null)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.42)' }} />
            <div className="om-sheet-up" style={{ position: 'relative', width: 380, maxWidth: '100%', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', padding: 22 }}>
              <h3 style={{ fontSize: 17, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Сменить статус на «{typeLabel}»?</h3>
              <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 2 }}>{confirmFor.lesson.dateLabel} · {confirmFor.lesson.time}</div>

              <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
                {/* Итоговый статус занятия */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
                  <Icon name="tag" size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />
                  <span style={{ color: 'var(--text-secondary)' }}>Статус занятия:</span>
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontWeight: 600, color: tgtTo.fg || 'var(--text)' }}><span style={{ width: 7, height: 7, borderRadius: '50%', background: tgtTo.dot }} />{tgtTo.label}</span>
                </div>
                {/* Изменение баланса */}
                {p.delta !== 0 ? (
                  <div style={{ display: 'flex', gap: 8, padding: '10px 12px', borderRadius: 'var(--radius-sm)', background: p.delta < 0 ? 'var(--coral-bg)' : 'var(--green-bg, rgba(0,180,143,0.12))', border: '1px solid ' + (p.delta < 0 ? '#E6C9C4' : '#BfE6D8') }}>
                    <Icon name={p.delta < 0 ? 'arrow-down-circle' : 'arrow-up-circle'} size={16} style={{ color: p.delta < 0 ? 'var(--coral)' : 'var(--green)', flex: 'none', marginTop: 1 }} />
                    <span style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.45 }}>
                      {p.delta < 0 ? 'Спишется с баланса ' : 'Вернётся на баланс '}<b style={{ color: 'var(--text)', fontFamily: 'var(--font-mono)' }}>{cdRub(Math.abs(p.delta))}</b>.<br />
                      Баланс: <span style={{ fontFamily: 'var(--font-mono)' }}>{cdRub(p.balanceBefore)}</span> → <b style={{ fontFamily: 'var(--font-mono)', color: p.balanceAfter < 0 ? 'var(--coral)' : 'var(--text)' }}>{cdRub(p.balanceAfter)}</b>
                    </span>
                  </div>
                ) : (
                  <div style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Баланс не изменится.</div>
                )}
                {/* Перерасчёт других занятий */}
                {p.flips.length > 0 && (
                  <div style={{ padding: '10px 12px', borderRadius: 'var(--radius-sm)', background: 'var(--gold-soft)', border: '1px solid #E7CFA3' }}>
                    <div style={{ display: 'flex', gap: 7, marginBottom: 6 }}><Icon name="alert-triangle" size={15} style={{ color: 'var(--gold-ink)', flex: 'none', marginTop: 1 }} /><span style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.4 }}>Из-за изменения баланса пересчитаются другие занятия:</span></div>
                    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                      {p.flips.slice(0, 5).map((f, i) => (
                        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11.5 }}>
                          <span style={{ color: 'var(--text-dim)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.label}</span>
                          <span style={{ fontWeight: 600, color: (LS[f.from] || {}).fg }}>{(LS[f.from] || {}).label}</span>
                          <Icon name="arrow-right" size={12} style={{ color: 'var(--text-dim)' }} />
                          <span style={{ fontWeight: 600, color: (LS[f.to] || {}).fg }}>{(LS[f.to] || {}).label}</span>
                        </div>
                      ))}
                      {p.flips.length > 5 && <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>…и ещё {p.flips.length - 5}</div>}
                    </div>
                  </div>
                )}
              </div>

              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 18 }}>
                <Button variant="ghost" onClick={() => setConfirmFor(null)}>Отмена</Button>
                <Button variant="primary" icon="check" onClick={() => { onChangeType(confirmFor.lesson, confirmFor.type); setConfirmFor(null); }}>Подтвердить</Button>
              </div>
            </div>
          </ModalLayer>
        );
      })()}
      {delFor && (
        <ModalLayer z={95}>
          <div onClick={() => setDelFor(null)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.4)' }} />
          <div className="om-sheet-up" style={{ position: 'relative', width: 340, 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="trash-2" 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)' }}>{delFor.dateLabel} · {(LS[delFor.status] || {}).label}</div>
              </div>
            </div>
            <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5, marginTop: 12 }}>Занятие исчезнет из истории.{(delFor.status === 'paid' || delFor.status === 'debt') ? ' Списанная сумма ' + cdRub(delFor.amount) + ' вернётся на баланс.' : ''} Действие нельзя отменить.</div>
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
              <Button variant="ghost" onClick={() => setDelFor(null)}>Отмена</Button>
              <Button variant="danger" icon="trash-2" onClick={() => { onDeleteLesson(delFor); setDelFor(null); }}>Удалить</Button>
            </div>
          </div>
        </ModalLayer>
      )}
    </React.Fragment>
  );
}

function contactMeta(type) {
  const all = (window.CRM_DATA && window.CRM_DATA.CONTACT_TYPES) || [];
  return all.find(t => t.value === type) || { label: type || 'Контакт', icon: 'at-sign', mono: false };
}

function ContactRow({ icon, label, value, mono, brand, onCopy }) {
  if (!value) return null;
  const clickable = !!onCopy;
  return (
    <div onClick={clickable ? () => onCopy(value) : undefined} title={clickable ? 'Нажмите, чтобы скопировать' : undefined}
      style={{ display: 'flex', alignItems: 'center', gap: 9, cursor: clickable ? 'pointer' : 'default', borderRadius: 'var(--radius-sm)', padding: clickable ? '3px 4px' : 0, margin: clickable ? '-3px -4px' : 0, transition: 'background var(--dur-fast) var(--ease)' }}
      onMouseEnter={clickable ? e => { e.currentTarget.style.background = 'var(--bg-soft)'; } : undefined}
      onMouseLeave={clickable ? e => { e.currentTarget.style.background = 'transparent'; } : undefined}>
      <span style={{ width: 26, height: 26, borderRadius: 6, background: 'var(--bg-soft)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: brand ? 'var(--brand-ink)' : 'var(--text-dim)', flex: 'none' }}>
        <Icon name={icon} size={14} />
      </span>
      <div style={{ minWidth: 0, flex: 1 }}>
        <div style={{ fontSize: 11.5, color: 'var(--text-dim)', lineHeight: 1.2 }}>{label}</div>
        <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--text)', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)' }}>{value}</div>
      </div>
      {clickable && <Icon name="copy" size={14} style={{ color: 'var(--text-dim)', flex: 'none' }} />}
    </div>
  );
}

function ContactList({ contacts, onCopy }) {
  const list = (contacts || []).filter(c => c.value);
  if (list.length === 0) return <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Контакты не указаны</div>;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {list.map((c, i) => {
        const m = contactMeta(c.type);
        return <ContactRow key={i} icon={m.icon} label={m.label} value={c.value} mono={m.mono} brand={!m.mono} onCopy={onCopy} />;
      })}
    </div>
  );
}

// Hoisted to module scope so they keep stable identity across ClientDetail
// re-renders (otherwise the whole subtree remounts and history rows re-flash).
function DetailSection({ title, count, children }) {
  return (
    <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', overflow: 'hidden', marginTop: 14 }}>
      <div style={{ padding: '10px 14px', background: 'var(--bg-soft)', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 8 }}>
        <Overline style={{ color: 'var(--text-secondary)' }}>{title}</Overline>
        {count != null && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: 'var(--text-dim)' }}>{count}</span>}
      </div>
      <div style={{ padding: 14 }}>{children}</div>
    </div>
  );
}

function MetaCell({ label, value, mono }) {
  return (
    <div>
      <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginBottom: 3 }}>{label}</div>
      <div style={{ fontSize: 13.5, fontWeight: 600, color: value ? 'var(--text)' : 'var(--text-dim)', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)' }}>{value || '—'}</div>
    </div>
  );
}

function HistoryRow({ h, greyed, onDelete, onRestore, removing }) {
  return (
    <div className={removing ? 'om-fly-right' : (greyed ? 'om-fade-in' : 'om-log-in')} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, opacity: greyed ? 0.45 : 1 }}>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--text-dim)', width: 110, flex: 'none' }}>{h.when}</span>
      <Pill kind={h.from} /><Icon name="arrow-right" size={13} style={{ color: 'var(--text-dim)' }} /><Pill kind={h.to} />
      <div style={{ flex: 1 }} />
      {onDelete && (
        <button onClick={() => onDelete(h.id)} title="Скрыть запись" style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--text-dim)', display: 'inline-flex', padding: 3 }}>
          <svg width="19" height="19" 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>
      )}
      {onRestore && (
        <button onClick={() => onRestore(h.id)} title="Вернуть" style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', display: 'inline-flex', padding: 3 }}>
          <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M9 14L4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/></svg>
        </button>
      )}
    </div>
  );
}

function ClientDetail({ client, onClose, onEdit, onArchive, onUpdate, onDelete }) {
  const [shown, setShown] = React.useState(false);
  const [deleting, setDeleting] = React.useState(false);
  const [quick, setQuick] = React.useState(false);
  const [bal, setBal] = React.useState(client ? client.balance : 0);
  const [flash, setFlash] = React.useState({ k: 0, dir: null });
  const [status, setStatus] = React.useState(client ? client.status : 'new');
  const [amount, setAmount] = React.useState('');
  const [notes, setNotes] = React.useState(client && client.notes ? client.notes : '');
  const [notesEdit, setNotesEdit] = React.useState(false);
  const saveNotes = () => { setNotesEdit(false); if (onUpdate) onUpdate(client.id, { notes: notes }); };
  const [history, setHistory] = React.useState(client && client.statusHistory ? client.statusHistory : []);
  const [hiddenIds, setHiddenIds] = React.useState([]);
  const [removingIds, setRemovingIds] = React.useState([]);
  const [showHidden, setShowHidden] = React.useState(false);
  const [copied, setCopied] = React.useState(false);
  const [toast, setToast] = React.useState(null);
  const toastTimer = React.useRef(null);
  const scrollRef = React.useRef(null);
  // Ширина модалки: тянется за левый край, по умолчанию шире (история получает +50% места).
  const [panelW, setPanelW] = React.useState(() => { try { const s = +localStorage.getItem('cd_panel_w'); return s ? Math.max(760, Math.min(1700, s)) : 1180; } catch (e) { return 1180; } });
  const startResize = (e) => {
    e.preventDefault();
    let last = panelW;
    const onMove = (ev) => { last = Math.max(760, Math.min(1700, window.innerWidth - ev.clientX)); setPanelW(last); };
    const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.userSelect = ''; try { localStorage.setItem('cd_panel_w', String(last)); } catch (e) {} };
    document.body.style.userSelect = 'none';
    document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
  };
  const historyRef = React.useRef(null);
  const firstRun = React.useRef(true);
  React.useEffect(() => { const t = setTimeout(() => setShown(true), 10); return () => clearTimeout(t); }, []);
  React.useEffect(() => {
    if (firstRun.current) { firstRun.current = false; return; }
    const sc = scrollRef.current; if (!sc) return;
    if (showHidden) { requestAnimationFrame(() => sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' })); }
    else if (historyRef.current) { sc.scrollTo({ top: Math.max(0, historyRef.current.offsetTop - 16), behavior: 'smooth' }); }
  }, [showHidden]);
  if (!client) return null;

  const close = () => { setShown(false); setTimeout(onClose, 220); };
  const edit = () => { setShown(false); setTimeout(() => onEdit && onEdit(client), 220); };
  const archive = () => { setShown(false); setTimeout(() => onArchive && onArchive(client), 220); };
  const flashToast = (msg) => { setToast(msg); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(null), 2200); };
  const copy = (value) => { if (navigator.clipboard) navigator.clipboard.writeText(value).catch(() => {}); flashToast('Скопировано: ' + value); };

  const STATUS_FLOW = ['new', 'call', 'trial', 'payment', 'active'];
  const now = () => { const d = new Date(); return `сегодня, ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; };
  const changeStatus = async (next) => {
    if (next === status) return;
    const prev = status;
    setStatus(next); // оптимистично
    const entry = { id: Date.now() + Math.random(), from: prev, to: next, when: now() };
    const nh = [entry, ...history]; setHistory(nh);
    try {
      await window.apiCall('/clients/' + client.id, { method: 'PATCH', body: JSON.stringify({ status: next }) });
      if (onUpdate) onUpdate(client.id, { status: next, statusHistory: nh });
    } catch (e) {
      setStatus(prev); setHistory(history); // откат
      flashToast('Не удалось сменить статус: ' + (e.message || 'ошибка'));
    }
  };

  // ---- Леджер: единый кошелёк, статусы занятий выводятся из баланса ----
  // Занятие, за которое преподавателю УЖЕ выплачено: {lesson, payout?, canConfirm, error} — предупреждение
  // перед удалением (сервер ответил 409 'lesson_in_payout'). payout приходит только тем, кто вправе видеть выплаты.
  const [paidWarn, setPaidWarn] = React.useState(null);
  const [ledgerV, setLedgerV] = React.useState(0);
  const [ledgerLog, setLedgerLog] = React.useState(client && client.ledgerLog ? client.ledgerLog : []);

  // Загрузка РЕАЛЬНЫХ занятий ученика из API (+ актуальный баланс). Маппим в форму кита;
  // статусы paid/debt/planned пересчитывает kit-овский applyLedger (тот же FIFO, что на сервере).
  const LMON = ['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек'];
  const reloadLessons = React.useCallback(async () => {
    try {
      const lz = await window.apiCall('/clients/' + client.id + '/lessons');
      const led = await window.apiCall('/clients/' + client.id + '/ledger').catch(() => null);
      const mapped = (lz.lessons || []).map((L) => {
        const d = new Date(L.scheduledAt);
        const kind = L.status === 'conducted' ? 'normal' : L.status === 'noshow' ? 'noshow' : L.status === 'cancelled' ? 'cancelled' : 'future';
        return {
          id: L.lessonId,
          dateISO: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`,
          dateLabel: `${d.getDate()} ${LMON[d.getMonth()]}`,
          time: `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`,
          teacher: L.teacherName || null, who: client.child,
          amount: Math.round((L.amountKopecks || 0) / 100),
          penalty: Math.round((L.penaltyKopecks || 0) / 100),
          kind, status: L.payment, topic: L.topic || '', homework: L.homework || '',
          teacherBonusRubles: L.teacherBonusRubles || 0,
          // Деньги преподавателя за занятие (приходят только тем, кто вправе видеть выплаты):
          // ставка НА ДАТУ занятия, итог со надбавкой и признак «уже в выплате» — для «Карточки занятия».
          teacherRateRubles: L.teacherRateRubles != null ? L.teacherRateRubles : null,
          teacherPayoutRubles: L.teacherPayoutRubles != null ? L.teacherPayoutRubles : null,
          payout: L.payout || null,
        };
      });
      if (led) { client.balance = Math.round((led.balanceKopecks || 0) / 100); setBal(client.balance); }
      Object.defineProperty(client, '__lessons', { value: mapped, enumerable: false, writable: true, configurable: true });
      window.applyLedger(client, mapped);
      setLedgerV((v) => v + 1);
    } catch (e) { /* нет права/ошибка — оставляем пусто */ }
  }, [client.id]);
  React.useEffect(() => { reloadLessons(); }, [reloadLessons]);
  const todayLabel = () => { const iso = window.APP_TODAY_ISO || '2026-05-31'; const p = iso.split('-'); const M = ['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек']; return `${+p[2]} ${M[+p[1]-1]} ${p[0]}`; };
  const pushLogs = (entries) => { if (!entries.length) return; setLedgerLog(prev => { const nl = [...entries.map(e => ({ id: Date.now() + Math.random(), when: todayLabel(), ...e })), ...prev]; if (onUpdate) onUpdate(client.id, { ledgerLog: nl }); return nl; }); };
  // Меняем баланс на delta, пересчитываем статусы занятий и логируем, какие долги погасились / ушли в долг.
  const applyBalanceDelta = (delta, mainLog) => {
    const lessons = ensureClientLessons(client);
    const beforeDebt = new Set(lessons.filter(l => l.status === 'debt').map(l => l.id));
    const beforePaid = new Set(lessons.filter(l => l.status === 'paid').map(l => l.id));
    const nb = (client.balance || 0) + delta;
    client.balance = nb; setBal(nb); if (onUpdate) onUpdate(client.id, { balance: nb, __lessons: lessons });
    window.applyLedger(client, lessons);
    const logs = mainLog ? [mainLog] : [];
    lessons.forEach(l => {
      if (beforeDebt.has(l.id) && l.status === 'paid') logs.push({ type: 'clear', lessonLabel: l.dateLabel, amount: l.amount, note: 'Погашено занятие' });
      if (beforePaid.has(l.id) && l.status === 'debt') logs.push({ type: 'debt', lessonLabel: l.dateLabel, amount: l.amount, note: 'Занятие ушло в долг' });
    });
    pushLogs(logs);
    setLedgerV(v => v + 1);
  };

  // Баланс пришёл с сервера (после реального пополнения) — ставим его и пересчитываем витрину.
  const syncBalanceFromServer = (balanceKopecks, mainLog) => {
    const nb = Math.round((balanceKopecks || 0) / 100);
    const lessons = ensureClientLessons(client);
    client.balance = nb; setBal(nb); if (onUpdate) onUpdate(client.id, { balance: nb });
    window.applyLedger(client, lessons);
    pushLogs(mainLog ? [mainLog] : []);
    setLedgerV(v => v + 1);
  };

  // Пополнение — на бэкенд (POST /clients/:id/topup): реальная транзакция + FIFO-погашение.
  // Списание пока локальное (отдельного эндпоинта нет).
  const topUp = async (sign) => {
    const n = parseInt(amount.replace(/\D/g, ''), 10); if (!n) return;
    if (sign > 0) {
      try {
        const res = await window.apiCall('/clients/' + client.id + '/topup', { method: 'POST', body: JSON.stringify({ amountKopecks: n * 100, note: 'Пополнение баланса' }) });
        syncBalanceFromServer(res.balanceKopecks, { type: 'topup', amount: n, note: 'Пополнение баланса' });
        setFlash(f => ({ k: f.k + 1, dir: 'up' })); setAmount('');
      } catch (e) { flashToast('Не удалось пополнить: ' + (e.message || 'ошибка')); }
    } else {
      try {
        const res = await window.apiCall('/clients/' + client.id + '/correction', { method: 'POST', body: JSON.stringify({ amountKopecks: -n * 100, note: 'Списание с баланса' }) });
        syncBalanceFromServer(res.balanceKopecks, { type: 'withdraw', amount: n, note: 'Списание с баланса' });
        setFlash(f => ({ k: f.k + 1, dir: 'down' })); setAmount('');
      } catch (e) { flashToast('Не удалось списать: ' + (e.message || 'ошибка')); }
    }
  };
  const copyLink = () => { setCopied(true); flashToast('Ссылка оплаты скопирована в буфер обмена'); setTimeout(() => setCopied(false), 2200); };
  // Список педагогов клиента + выбранный для быстрого пополнения (цена зависит от педагога).
  const cTeachers = (client.teachers && client.teachers.length) ? client.teachers : (client.teacher ? [client.teacher] : []);
  const [quickTeacher, setQuickTeacher] = React.useState(cTeachers[0] || null);
  const quickPrice = priceFor(client, quickTeacher);
  const quickAdjust = async (sign, lessons) => {
    const price = priceFor(client, quickTeacher); const n = lessons * price;
    const who = quickTeacher ? ' · ' + shortTeacher(quickTeacher) : '';
    if (sign > 0) {
      try {
        const res = await window.apiCall('/clients/' + client.id + '/topup', { method: 'POST', body: JSON.stringify({ amountKopecks: n * 100, note: 'Пополнение: ' + lessons + ' зан.' + who }) });
        syncBalanceFromServer(res.balanceKopecks, { type: 'topup', amount: n, note: 'Пополнение: ' + lessons + ' зан.' + who });
        setFlash(f => ({ k: f.k + 1, dir: 'up' }));
        flashToast('Пополнено: ' + lessons + ' зан. · ' + new Intl.NumberFormat('ru-RU').format(n) + ' ₽'); setQuick(false);
      } catch (e) { flashToast('Не удалось пополнить: ' + (e.message || 'ошибка')); }
    } else {
      try {
        const res = await window.apiCall('/clients/' + client.id + '/correction', { method: 'POST', body: JSON.stringify({ amountKopecks: -n * 100, note: 'Списание: ' + lessons + ' зан.' + who }) });
        syncBalanceFromServer(res.balanceKopecks, { type: 'withdraw', amount: n, note: 'Списание: ' + lessons + ' зан.' + who });
        setFlash(f => ({ k: f.k + 1, dir: 'down' }));
        flashToast('Списано: ' + lessons + ' зан. · ' + new Intl.NumberFormat('ru-RU').format(n) + ' ₽'); setQuick(false);
      } catch (e) { flashToast('Не удалось списать: ' + (e.message || 'ошибка')); }
    }
  };
  // «Сколько занятие списывает с кошелька»: normal → стоимость, noshow → штраф, иначе 0.
  const lessonCharge = (l) => (l.kind === 'normal' ? (l.amount || 0) : 0) + (l.kind === 'noshow' ? (l.penalty || 0) : 0);
  // Смена факта занятия — на бэкенд (PATCH /lessons/:id/status): движок сам спишет/вернёт
  // через транзакцию баланса. Затем перечитываем занятия и баланс с сервера (источник правды).
  const LESSON_KIND_TO_STATUS = { normal: 'conducted', noshow: 'noshow', cancelled: 'cancelled', future: 'scheduled' };
  const applyLesson = async (l, newKind, newPenalty, note) => {
    const status = LESSON_KIND_TO_STATUS[newKind] || 'conducted';
    try {
      const body = { status };
      if (newKind === 'noshow') { body.noshowCharged = true; if (newPenalty != null) body.penaltyKopecks = Math.round(newPenalty * 100); }
      await window.apiCall('/lessons/' + l.id + '/status', { method: 'PATCH', body: JSON.stringify(body) });
      await reloadLessons();
      if (onUpdate) onUpdate(client.id, { balance: client.balance });
      flashToast(note || 'Занятие обновлено');
    } catch (e) { flashToast('Не удалось обновить занятие: ' + (e.message || 'ошибка')); }
  };
  // Предпросмотр последствий смены типа занятия — БЕЗ изменения данных.
  // Возвращает: списание/возврат с баланса, новый баланс и какие занятия сменят «Оплачено↔В долг».
  const previewLessonChange = (l, newKind, newPenalty) => {
    const lessons = ensureClientLessons(client);
    const beforeStatus = {}; lessons.forEach(x => { beforeStatus[x.id] = x.status; });
    const chargeOf = (kind, amount, penalty) => (kind === 'normal' ? (amount || 0) : 0) + (kind === 'noshow' ? (penalty || 0) : 0);
    const after = chargeOf(newKind, l.amount, newKind === 'noshow' ? (newPenalty != null ? newPenalty : (l.penalty || 0)) : 0);
    const delta = -(after - lessonCharge(l));
    const newBalance = (client.balance || 0) + delta;
    const sim = lessons.map(x => ({ id: x.id, dateLabel: x.dateLabel, dateISO: x.dateISO, kind: x === l ? newKind : x.kind, amount: x.amount, penalty: x === l ? (newKind === 'noshow' ? (newPenalty != null ? newPenalty : (x.penalty || 0)) : 0) : x.penalty, status: x.status }));
    window.applyLedger({ balance: newBalance }, sim);
    const flips = [];
    sim.forEach(s => { const was = beforeStatus[s.id]; if (s.id !== l.id && was !== s.status && (was === 'paid' || was === 'debt') && (s.status === 'paid' || s.status === 'debt')) flips.push({ label: s.dateLabel, from: was, to: s.status }); });
    const tgt = sim.find(s => s.id === l.id);
    return { delta, balanceBefore: client.balance || 0, balanceAfter: newBalance, targetFrom: beforeStatus[l.id], targetTo: tgt ? tgt.status : null, flips };
  };
  // Смена типа занятия из меню. «Оплачено/В долг» — системные (выводятся из баланса), вручную не ставятся.
  const changeLessonType = (l, type) => {
    const today = window.APP_TODAY_ISO || '2026-05-31';
    const future = l.dateISO > today;
    if ((type === 'normal' || type === 'noshow') && future) {
      flashToast('Будущее занятие нельзя отметить проведённым. Чтобы предоплатить — внесите деньги на баланс');
      return false;
    }
    if (type === 'future' && !future) {
      flashToast('Прошедшее занятие нельзя сделать запланированным — оно станет «Оплачено» или «В долг»');
      return false;
    }
    if (type === 'noshow') return 'penalty'; // откроем модалку штрафа
    if (type === 'normal') {
      applyLesson(l, 'normal', 0, 'Занятие проведено · списание');
      // Сообщаем итог: при нехватке средств занятие уходит «В долг», а не «Оплачено».
      if (l.status === 'debt') flashToast('Отмечено «Проведено», но занятие «В долг»: на балансе недостаточно средств (' + cdRub(client.balance || 0) + '). Пополните баланс — оно станет «Оплачено»');
      else flashToast('Отмечено «Проведено»' + (l.amount ? ' · списано ' + cdRub(l.amount) + ', занятие оплачено' : ''));
    }
    else if (type === 'cancelled') { applyLesson(l, 'cancelled', 0, 'Отмена занятия · возврат'); flashToast('Занятие отменено'); }
    else if (type === 'future') { applyLesson(l, 'future', 0, 'Перенос в запланированные · возврат'); flashToast('Отмечено «Запланировано»'); }
    return true;
  };
  // Отмена занятия → возврат суммы на баланс (если было списано).
  const cancelLesson = (l) => { const charged = lessonCharge(l); applyLesson(l, 'cancelled', 0, 'Отмена занятия · возврат'); flashToast('Занятие отменено' + (charged ? ' · возврат ' + cdRub(charged) : '')); };
  // Создание занятия на бэкенде (POST /lessons): цена — снимок из enrollment ученика.
  // Если задан тип (провёл/пропуск/отмена) — сразу переводим факт через PATCH.
  const addLesson = async (draft) => {
    try {
      const at = new Date((draft.dateISO || '') + 'T' + cdNormTime(draft.time || '17:00') + ':00');
      const st = (window.CRM_DATA.STAFF || []).find(s => s.name === draft.teacher);
      // Разовая цена (в рублях из формы → копейки). >0 → шлём; иначе бэк возьмёт обычную цену пары.
      const amtRub = parseInt(String(draft.amount || '').replace(/\D/g, ''), 10);
      const res = await window.apiCall('/lessons', { method: 'POST', body: JSON.stringify({
        clientId: client.id, teacherId: st ? st.id : undefined, 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) });
      }
      await reloadLessons();
      if (onUpdate) onUpdate(client.id, { balance: client.balance });
      flashToast('Занятие добавлено');
    } catch (e) { flashToast('Не удалось добавить занятие: ' + (e.message || 'ошибка')); }
  };
  // Правка существующего занятия из «Карточки занятия». Планируемое: дата/время/цена/надбавка/тема/ДЗ +
  // смена статуса. Проведённое: тема/ДЗ (бэк отклонит деньги/время); надбавку шлём только если ИЗМЕНИЛАСЬ
  // (иначе у выплаченного занятия сработал бы гвард «уже в выплате» даже без изменений).
  const dbStatusOf = (paymentStatus) => ({ paid: 'conducted', debt: 'conducted', planned: 'scheduled', noshow: 'noshow', cancelled: 'cancelled' }[paymentStatus] || 'scheduled');
  const updateLessonFromCard = async (draft) => {
    try {
      const cur = (ensureClientLessons(client) || []).find(x => x.id === draft.id) || {};
      const isPlanned = cur.status === 'planned';
      const patch = { topic: draft.topic || '', homework: draft.homework || '' };
      if (isPlanned) {
        const at = new Date((draft.dateISO || '') + 'T' + cdNormTime(draft.time || '17:00') + ':00');
        patch.scheduledAt = at.toISOString();
        const amtRub = parseInt(String(draft.amount || '').replace(/\D/g, ''), 10);
        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 kindToStatus = { normal: 'conducted', noshow: 'noshow', cancelled: 'cancelled', future: 'scheduled' };
      const want = kindToStatus[draft.type];
      if (want && want !== dbStatusOf(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) });
      }
      await reloadLessons();
      if (onUpdate) onUpdate(client.id, { balance: client.balance });
      flashToast('Занятие обновлено');
    } catch (e) { flashToast('Не удалось сохранить занятие: ' + (e.message || 'ошибка')); }
  };
  const saveLessonFromCard = (draft) => (draft.id != null ? updateLessonFromCard(draft) : addLesson(draft));
  // Удаление занятия на бэкенде (DELETE /lessons/:id) — движок вернёт деньги, если было списание.
  // Если преподавателю за занятие УЖЕ выплачено, сервер отвечает 409 'lesson_in_payout' →
  // показываем предупреждение с подтверждением (см. paidWarn). Поведение при подтверждении прежнее:
  // выплата не трогается (вернуть деньги преподу = сторно выплаты в его карточке).
  const deleteLesson = async (l, confirmPaid) => {
    try {
      await window.apiCall('/lessons/' + l.id + (confirmPaid ? '?confirmPaid=1' : ''), { method: 'DELETE' });
      setPaidWarn(null);
      await reloadLessons();
      if (onUpdate) onUpdate(client.id, { balance: client.balance });
      flashToast('Занятие удалено');
    } catch (e) {
      if (e.status === 409 && e.code === 'lesson_in_payout') { setPaidWarn({ lesson: l, ...(e.data || {}) }); return; }
      setPaidWarn(null);
      flashToast('Не удалось удалить занятие: ' + (e.message || 'ошибка'));
    }
  };
  const setNoShowPenalty = (l, penalty) => {
    applyLesson(l, 'noshow', penalty, 'Штраф за неявку');
    flashToast(penalty ? 'Штраф ' + cdRub(penalty) + ' списан' : 'Отмечено «Не пришёл»');
  };
  const remove = () => { if (onDelete) onDelete(client, () => setDeleting(true)); };
  // Оплата преподавателю — НЕизменна админом, считается от ставки преподавателя на сегодня.
  // Если у преподавателя запланировано повышение — показываем его дату и будущую цифру.
  const tpStaff = (window.CRM_DATA.STAFF || []).find(s => s.name === client.teacher && s.role === 'teacher');
  const tpTodayISO = window.APP_TODAY_ISO || '2026-05-31';
  const tpNow = tpStaff ? (window.rateOn ? window.rateOn(tpStaff, tpTodayISO) : tpStaff.rate) : null;
  const tpNext = tpStaff && tpStaff.rateHistory ? tpStaff.rateHistory.filter(e => e.from > tpTodayISO).sort((a, b) => (a.from < b.from ? -1 : 1))[0] : null;
  const tpRu = (iso) => { const p = String(iso).split('-'); return p.length === 3 ? `${+p[2]}.${p[1]}.${p[0]}` : iso; };

  const parents = client.parents || [];
  const visibleHistory = history.filter(h => !hiddenIds.includes(h.id));
  const hiddenHistory = history.filter(h => hiddenIds.includes(h.id));
  const hideEntry = (id) => { setRemovingIds(p => [...p, id]); setTimeout(() => { setHiddenIds(p => [...p, id]); setRemovingIds(p => p.filter(x => x !== id)); }, 240); };

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 40 }}>
      <div onClick={close} style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.42)', backdropFilter: 'blur(2px)', opacity: shown ? 1 : 0, transition: 'opacity var(--dur) var(--ease)' }} />
      <aside style={{
        position: 'absolute', top: 0, right: 0, bottom: 0, width: panelW + 'px', maxWidth: '97vw', background: 'var(--bg-card)',
        boxShadow: 'var(--shadow-lg)', display: 'flex', flexDirection: 'column',
        transform: shown && !deleting ? 'translateX(0)' : 'translateX(100%)', opacity: deleting ? 0 : 1,
        transition: deleting ? 'transform 260ms cubic-bezier(0.55,0,1,0.45), opacity 260ms var(--ease)' : 'transform 280ms cubic-bezier(0.22, 1, 0.36, 1)',
      }}>
        {/* Ручка изменения ширины (тянуть за левый край) */}
        <div onMouseDown={startResize} title="Потяните, чтобы изменить ширину" style={{ position: 'absolute', top: 0, left: -4, width: 10, height: '100%', cursor: 'col-resize', zIndex: 6, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
          onMouseEnter={e => { e.currentTarget.firstChild.style.background = 'var(--brand)'; e.currentTarget.firstChild.style.color = '#fff'; }} onMouseLeave={e => { e.currentTarget.firstChild.style.background = 'var(--bg-soft)'; e.currentTarget.firstChild.style.color = 'var(--text-dim)'; }}>
          <div style={{ width: 7, height: 40, borderRadius: 4, background: 'var(--bg-soft)', border: '1px solid var(--border)', color: 'var(--text-dim)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2.5, transition: 'background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease)' }}>
            <span style={{ width: 2, height: 2, borderRadius: '50%', background: 'currentColor' }} />
            <span style={{ width: 2, height: 2, borderRadius: '50%', background: 'currentColor' }} />
            <span style={{ width: 2, height: 2, borderRadius: '50%', background: 'currentColor' }} />
          </div>
        </div>
        {/* Header */}
        <div style={{ padding: '18px 22px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 13 }}>
          <Avatar animal={client.animal} tone={client.tone} size={46} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Клиент</div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <h2 style={{ fontSize: 19, fontWeight: 700, letterSpacing: '-0.3px', margin: 0, color: 'var(--text)' }}>{client.child}</h2>
              {client.self && <span style={{ fontSize: 11, fontWeight: 600, color: 'var(--gold-ink)', background: 'var(--gold-soft)', border: '1px solid #D8C291', borderRadius: 3, padding: '1px 5px' }}>сам.</span>}
            </div>
          </div>
          <button onClick={close} title="Закрыть" 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' }}>
            <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>

        <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
        <div ref={scrollRef} style={{ flex: 1, minWidth: 0, overflowY: 'auto', overflowX: 'hidden', padding: '18px 22px', borderRight: '1px solid var(--border)' }}>
          {/* Balance block */}
          <div style={{ background: 'var(--bg-soft)', borderRadius: 'var(--radius-md)', padding: 16 }}>
            <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
              <div>
                <Overline>Баланс</Overline>
                <div key={flash.k} style={{ fontSize: 26, fontWeight: 700, fontFamily: 'var(--font-mono)', marginTop: 6, color: bal > 0 ? 'var(--green)' : bal < 0 ? 'var(--coral)' : 'var(--text)', animation: flash.dir ? `om-pulse-${flash.dir} 0.7s ease` : 'none', transformOrigin: 'left center', display: 'inline-block' }}>
                  {new Intl.NumberFormat('ru-RU').format(bal)} ₽
                </div>
                <div style={{ fontSize: 12.5, color: 'var(--text-dim)', marginTop: 2 }}>Занятие: <span style={{ fontFamily: 'var(--font-mono)' }}>{(() => { if (!cTeachers.length) return '—'; const ps = cTeachers.map(t => priceFor(client, t)); const mn = Math.min(...ps), mx = Math.max(...ps); const f = (n) => new Intl.NumberFormat('ru-RU').format(n); return mn === mx ? f(mn) + ' ₽' : f(mn) + '–' + f(mx) + ' ₽'; })()}</span>{cTeachers.length > 1 && <span style={{ marginLeft: 4 }}>· зависит от педагога</span>}</div>
              </div>
              <button onClick={copyLink}
                onMouseEnter={e => { if (!copied) e.currentTarget.style.background = '#D3DEF4'; }}
                onMouseLeave={e => { if (!copied) e.currentTarget.style.background = 'var(--brand-soft)'; }}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 12px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--brand)', background: copied ? 'var(--green-bg)' : 'var(--brand-soft)', color: copied ? 'var(--green-ink)' : 'var(--brand-ink)', borderColor: copied ? 'var(--green)' : 'var(--brand)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', flex: 'none', alignSelf: 'flex-start', transition: 'background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease)' }}>
                <span key={copied ? 'c' : 'l'} className="om-fade-in" style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                  <Icon name={copied ? 'check' : 'link'} size={15} />{copied ? 'Скопировано' : 'Ссылка оплаты'}
                </span>
              </button>
            </div>
            <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
              <input value={amount} onChange={e => setAmount(e.target.value)} placeholder="Сумма в ₽" style={{ flex: 1, minWidth: 0, border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', padding: '8px 11px', fontFamily: 'var(--font-mono)', fontSize: 13.5, color: 'var(--text)', outline: 'none', background: 'var(--bg-card)' }} />
              <button onClick={() => topUp(1)}
                onMouseEnter={e => { e.currentTarget.style.background = '#28593F'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--green)'; }}
                style={{ padding: '8px 12px', borderRadius: 'var(--radius-sm)', border: 'none', background: 'var(--green)', color: '#fff', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', transition: 'background var(--dur-fast) var(--ease)' }}>Пополнить</button>
              <button onClick={() => topUp(-1)}
                onMouseEnter={e => { e.currentTarget.style.background = '#8E3A32'; }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--coral)'; }}
                style={{ padding: '8px 12px', borderRadius: 'var(--radius-sm)', border: 'none', background: 'var(--coral)', color: '#fff', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', transition: 'background var(--dur-fast) var(--ease)' }}>Списать</button>
            </div>
            <button onClick={() => setQuick(true)}
              onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--brand)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'var(--border-strong)'; }}
              style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7, width: '100%', marginTop: 8, padding: '9px', borderRadius: 'var(--radius-sm)', border: '1px dashed var(--border-strong)', background: 'transparent', color: 'var(--brand-ink)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, cursor: 'pointer', transition: 'background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease)' }}>
              <Icon name="zap" size={15} />Быстрое пополнение
            </button>
          </div>

          {/* Ученик */}
          <DetailSection title={client.self ? 'Ученик (взрослый)' : 'Ученик'}>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <ContactRow icon="user" label="ФИО" value={client.child} />
              <ContactList contacts={client.contacts} onCopy={copy} />
            </div>
          </DetailSection>

          {/* Родители */}
          {!client.self && (
            <DetailSection title={parents.length > 1 ? 'Родители' : 'Родитель'} count={parents.length || null}>
              {parents.length === 0 && <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Родитель не добавлен</div>}
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
                {parents.map((p, i) => (
                  <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 10, paddingTop: i ? 14 : 0, borderTop: i ? '1px dashed var(--border)' : 'none' }}>
                    <ContactRow icon="user" label={parents.length > 1 ? `Родитель ${i + 1}` : 'ФИО'} value={p.name} />
                    <ContactList contacts={p.contacts} onCopy={copy} />
                  </div>
                ))}
              </div>
            </DetailSection>
          )}

          {/* Статистика по клиенту */}
          {(() => {
            const ls = (ensureClientLessons(client) || []);
            const conductedL = ls.filter(l => l.status === 'paid' || l.status === 'debt' || l.status === 'noshow');
            const lessonsCount = conductedL.length;
            // Доход с клиента — сумма стоимостей проведённых занятий (оплачено + в долг) + взятые штрафы.
            const income = ls.filter(l => l.status === 'paid' || l.status === 'debt').reduce((a, l) => a + (l.amount || 0), 0)
              + ls.filter(l => l.status === 'noshow').reduce((a, l) => a + (l.penalty || 0), 0);
            // Выручка (маржа) — доход минус выплаты педагогам за эти занятия (ставка педагога на дату занятия).
            const teacherCost = conductedL.reduce((a, l) => { const st = (window.CRM_DATA.STAFF || []).find(s => s.name === l.teacher && s.role === 'teacher'); const pay = st ? (window.rateOn ? window.rateOn(st, l.dateISO) : (st.rate || 0)) : 0; return a + pay; }, 0);
            const margin = income - teacherCost;
            const fmt = (n) => new Intl.NumberFormat('ru-RU').format(n);
            const cell = (label, value, color) => (
              <div style={{ flex: 1, minWidth: 0, padding: '10px 12px', background: 'var(--bg-soft)', borderRadius: 'var(--radius-sm)' }}>
                <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 3, whiteSpace: 'nowrap' }}>{label}</div>
                <div style={{ fontFamily: 'var(--font-mono)', fontSize: 16, fontWeight: 700, color: color || 'var(--text)' }}>{value}</div>
              </div>
            );
            return (
              <DetailSection title="Статистика">
                <div style={{ display: 'flex', gap: 8 }}>
                  {cell('Занятий', String(lessonsCount))}
                  {cell('Доход с клиента', fmt(income) + ' ₽', 'var(--green)')}
                  {cell('Выручка', (margin >= 0 ? '' : '−') + fmt(Math.abs(margin)) + ' ₽', margin >= 0 ? 'var(--brand-ink)' : 'var(--coral)')}
                </div>
                <div style={{ fontSize: 10.5, color: 'var(--text-dim)', marginTop: 8, lineHeight: 1.4 }}>Доход — сумма проведённых занятий и штрафов. Выручка — доход за вычетом выплат педагогам.</div>
              </DetailSection>
            );
          })()}

          {/* Детали */}
          <DetailSection title="Детали">
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px 18px' }}>
              <div>
                <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginBottom: 5 }}>Статус</div>
                <span key={status} className="om-pop"><Pill kind={status} /></span>
              </div>
              <MetaCell label="Источник" value={client.source} />
              <div>
                <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginBottom: 5 }}>{(client.directions && client.directions.length > 1) ? 'Направления' : 'Направление'}</div>
                {(() => { const dirs = (client.directions && client.directions.length) ? client.directions : (client.direction ? [client.direction] : []); return dirs.length ? <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>{dirs.map(d => <DirChip key={d}>{d}</DirChip>)}</div> : <DirChip>{null}</DirChip>; })()}
              </div>
              <div style={{ gridColumn: '1 / -1' }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 5 }}>
                  <span style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>{(() => { const ts = (client.teachers && client.teachers.length) ? client.teachers : (client.teacher ? [client.teacher] : []); return ts.length > 1 ? 'Преподаватели' : 'Преподаватель'; })()}</span>
                  <span style={{ fontSize: 10.5, color: 'var(--text-dim)' }}>оплата педагогу / занятие</span>
                </div>
                {(() => {
                  const ts = (client.teachers && client.teachers.length) ? client.teachers : (client.teacher ? [client.teacher] : []);
                  if (!ts.length) return <div style={{ fontSize: 14, color: 'var(--text-dim)' }}>—</div>;
                  const tsub = client.teacherSubjects || {};
                  const payOf = (t) => { const st = (window.CRM_DATA.STAFF || []).find(s => s.name === t && s.role === 'teacher'); if (!st) return null; const now = window.rateOn ? window.rateOn(st, tpTodayISO) : st.rate; const next = st.rateHistory ? st.rateHistory.filter(e => e.from > tpTodayISO).sort((a, b) => (a.from < b.from ? -1 : 1))[0] : null; return { now, next }; };
                  return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{ts.map(t => { const subs = tsub[t] || []; const pay = payOf(t); return (
                    <div key={t} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      <div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                        <span style={{ fontSize: 14, color: 'var(--text)', fontWeight: 500 }}>{shortTeacher(t)}</span>
                        {subs.length
                          ? <span style={{ display: 'inline-flex', flexWrap: 'wrap', gap: 4 }}>{subs.map(s => <DirChip key={s}>{s}</DirChip>)}</span>
                          : <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--coral)' }}>предмет не выбран</span>}
                      </div>
                      {/* Оплата педагогу — у каждого своя (от его ставки на сегодня), справа от ФИО */}
                      <div style={{ flex: 'none', textAlign: 'right' }}>
                        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontFamily: pay && pay.now != null ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, color: pay && pay.now != null ? 'var(--text)' : 'var(--text-dim)' }}>
                          {pay && pay.now != null ? new Intl.NumberFormat('ru-RU').format(pay.now) + ' ₽' : '—'}
                          <span title="Оплата педагогу считается от его ставки на сегодня — изменить нельзя" style={{ color: 'var(--text-dim)', display: 'inline-flex' }}><Icon name="lock" size={11} /></span>
                        </div>
                        {pay && pay.next && <div style={{ fontSize: 10, color: 'var(--text-dim)', marginTop: 1 }}>с {tpRu(pay.next.from)} → {new Intl.NumberFormat('ru-RU').format(pay.next.rate)} ₽</div>}
                      </div>
                    </div>
                  ); })}</div>;
                })()}
              </div>
              <MetaCell label="Менеджер" value={client.manager} mono />
              {status === 'refusal' && <MetaCell label="Причина отказа" value={client.refuseReason} />}
            </div>
          </DetailSection>

          {/* Заметки по ученику */}
          <DetailSection title="Заметки">
            {notesEdit ? (
              <div>
                <textarea autoFocus value={notes} onChange={e => setNotes(e.target.value)} placeholder="Особенности ученика, договорённости, важные детали…" rows={4} 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)', lineHeight: 1.5 }} />
                <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 10 }}>
                  <Button variant="ghost" size="sm" onClick={() => { setNotes(client.notes || ''); setNotesEdit(false); }}>Отмена</Button>
                  <Button variant="primary" size="sm" icon="check" onClick={saveNotes}>Сохранить</Button>
                </div>
              </div>
            ) : (
              notes && notes.trim() ? (
                <div onClick={() => setNotesEdit(true)} style={{ cursor: 'text', fontSize: 13.5, color: 'var(--text-secondary)', lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>
                  {notes}
                  <button onClick={e => { e.stopPropagation(); setNotesEdit(true); }} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 600, verticalAlign: 'middle' }}><Icon name="pencil" size={12} />Изменить</button>
                </div>
              ) : (
                <button onClick={() => setNotesEdit(true)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: '1px dashed var(--border-strong)', background: 'var(--bg-card)', borderRadius: 'var(--radius-sm)', padding: '9px 12px', cursor: 'pointer', color: 'var(--text-dim)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600 }}>
                  <Icon name="plus" size={14} />Добавить заметку
                </button>
              )
            )}
          </DetailSection>

          {/* Change status */}
          <div style={{ marginTop: 22 }}>
            <Overline style={{ marginBottom: 10 }}>Сменить статус</Overline>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
              {STATUS_FLOW.concat('refusal').map(s => {
                const on = s === status;
                const p = PILL[s];
                return (
                  <button key={s} onClick={() => changeStatus(s)}
                    onMouseEnter={e => { if (s !== status) e.currentTarget.style.background = 'var(--bg-soft)'; }}
                    onMouseLeave={e => { if (s !== status) e.currentTarget.style.background = 'var(--bg-card)'; }}
                    style={{
                    display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 11px', cursor: 'pointer',
                    borderRadius: 'var(--radius-pill)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600,
                    border: '1px solid ' + (on ? p.dot : (s === 'refusal' ? 'var(--coral-bg)' : 'var(--border)')),
                    background: 'var(--bg-card)', color: s === 'refusal' ? 'var(--coral)' : (on ? p.fg : 'var(--text-secondary)'),
                    boxShadow: on ? ('inset 0 0 0 1px ' + p.dot) : 'none',
                    transition: 'border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease), color var(--dur) var(--ease), background var(--dur-fast) var(--ease)',
                  }}>
                    <span style={{ width: 7, height: 7, borderRadius: '50%', background: p.dot }} />{p.label}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Status history */}
          <div ref={historyRef} style={{ marginTop: 22 }}>
            <Overline style={{ marginBottom: 10 }}>История статусов</Overline>
            {history.length === 0 && <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>Изменений в этой сессии нет — смените статус выше.</div>}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {visibleHistory.map(h => <HistoryRow key={h.id} h={h} removing={removingIds.includes(h.id)} onDelete={hideEntry} />)}
            </div>
            {hiddenHistory.length > 0 && (
              <div style={{ marginTop: 12 }}>
                <button onClick={() => setShowHidden(v => !v)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: 'none', background: 'none', cursor: 'pointer', color: 'var(--brand-ink)', fontSize: 12.5, fontWeight: 600, fontFamily: 'var(--font-sans)', padding: 0 }}>
                  <Icon name={showHidden ? 'eye-off' : 'eye'} size={14} />{showHidden ? 'Скрыть удалённые' : `Показать удалённые (${hiddenHistory.length})`}
                </button>
                {showHidden && (
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
                    {hiddenHistory.map(h => <HistoryRow key={h.id} h={h} greyed onRestore={(id) => setHiddenIds(prev => prev.filter(x => x !== id))} />)}
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
        <div style={{ flex: 1.5, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
          <LessonHistoryPanel client={client} ledgerV={ledgerV} ledgerLog={ledgerLog} onCancelLesson={cancelLesson} onSetPenalty={setNoShowPenalty} onDeleteLesson={deleteLesson} onChangeType={changeLessonType} onAddLesson={addLesson} onSaveLesson={saveLessonFromCard} onPreview={previewLessonChange} />
        </div>
        </div>

        {/* Footer */}
        <div style={{ padding: '14px 22px', borderTop: '1px solid var(--border)', display: 'flex', gap: 10 }}>
          <Button variant="secondary" icon="edit-3" onClick={edit}>Редактировать</Button>
          <Button variant="secondary" icon="archive" onClick={archive}>{client.archived ? 'Из архива' : 'В архив'}</Button>
          <div style={{ flex: 1 }} />
          <Button variant="danger" icon="close-tile" onClick={remove}>Удалить</Button>
        </div>

        {/* Quick balance panel (slides up over the drawer) */}
        {quick && (
          <div style={{ position: 'absolute', inset: 0, zIndex: 5, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
            <div onClick={() => setQuick(false)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.35)', backdropFilter: 'blur(1px)' }} />
            <div className="om-sheet-up" style={{ position: 'relative', background: 'var(--bg-card)', borderTop: '1px solid var(--border)', borderRadius: '14px 14px 0 0', boxShadow: '0 -10px 30px rgba(17,24,39,0.14)', padding: '18px 20px 20px' }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
                <h3 style={{ fontSize: 16, fontWeight: 700, margin: 0, color: 'var(--text)' }}>Быстрое пополнение</h3>
                <button onClick={() => setQuick(false)} title="Закрыть" style={{ width: 30, height: 30, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
                  <svg width="16" height="16" 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>
              <div style={{ marginBottom: 14 }}>
                {cTeachers.length > 1 && (
                  <div style={{ marginBottom: 10 }}>
                    <div style={{ fontSize: 11.5, color: 'var(--text-dim)', marginBottom: 6 }}>Педагог (от него зависит цена)</div>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                      {cTeachers.map(t => { const on = t === quickTeacher; return (
                        <button key={t} onClick={() => setQuickTeacher(t)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 11px', borderRadius: 'var(--radius-pill)', border: '1px solid ' + (on ? 'var(--brand)' : 'var(--border)'), background: on ? 'var(--brand-soft)' : 'var(--bg-card)', color: on ? 'var(--brand-ink)' : 'var(--text-secondary)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600 }}>{shortTeacher(t)} · {new Intl.NumberFormat('ru-RU').format(priceFor(client, t))} ₽</button>
                      ); })}
                    </div>
                  </div>
                )}
                <div style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Занятие: <span style={{ fontFamily: 'var(--font-mono)' }}>{new Intl.NumberFormat('ru-RU').format(quickPrice)} ₽</span></div>
              </div>
              <div style={{ fontSize: 11.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-dim)', marginBottom: 8 }}>Пополнить</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, marginBottom: 16 }}>
                {[1, 2, 8, 10].map(n => (
                  <button key={n} onClick={() => quickAdjust(1, n)}
                    onMouseEnter={e => { e.currentTarget.style.background = 'var(--green-bg)'; e.currentTarget.style.borderColor = 'var(--green)'; }}
                    onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
                    style={{ padding: '10px 4px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', transition: 'background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease)' }}>
                    <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--green)', fontFamily: 'var(--font-mono)' }}>+{n}</div>
                    <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 2 }}>зан.</div>
                    <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', marginTop: 2 }}>{new Intl.NumberFormat('ru-RU').format(n * quickPrice)} ₽</div>
                  </button>
                ))}
              </div>
              <div style={{ fontSize: 11.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-dim)', marginBottom: 8 }}>Списать</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
                {[1, 2, 8, 10].map(n => (
                  <button key={n} onClick={() => quickAdjust(-1, n)}
                    onMouseEnter={e => { e.currentTarget.style.background = 'var(--coral-bg)'; e.currentTarget.style.borderColor = 'var(--coral)'; }}
                    onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
                    style={{ padding: '10px 4px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', transition: 'background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease)' }}>
                    <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--coral)', fontFamily: 'var(--font-mono)' }}>−{n}</div>
                    <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 2 }}>зан.</div>
                    <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', marginTop: 2 }}>{new Intl.NumberFormat('ru-RU').format(n * quickPrice)} ₽</div>
                  </button>
                ))}
              </div>
            </div>
          </div>
        )}

        {/* Занятие уже оплачено преподавателю — предупреждение перед удалением (сервер: 409 lesson_in_payout).
            Это ТОЛЬКО предупреждение: подтвердил → удаляем как раньше, выплату не трогаем. */}
        {paidWarn && (() => {
          const p = paidWarn.payout; // приходит только тем, кто вправе видеть выплаты (админ)
          const ru = (iso) => { const a = String(iso || '').slice(0, 10).split('-'); return a.length === 3 ? `${+a[2]} ${['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек'][+a[1] - 1]} ${a[0]}` : '—'; };
          return (
            <ModalLayer z={95}>
              <div onClick={() => setPaidWarn(null)} className="om-fade-in" style={{ position: 'absolute', inset: 0, background: 'rgba(21,28,46,0.45)' }} />
              <div className="om-sheet-up" style={{ position: 'relative', width: 400, 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="alert-triangle" 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)' }}>{paidWarn.lesson.dateLabel}{paidWarn.lesson.time ? ' · ' + paidWarn.lesson.time : ''}</div>
                  </div>
                </div>
                {p && (
                  <div style={{ marginTop: 14, padding: '10px 12px', background: 'var(--bg-soft)', borderRadius: 'var(--radius-md)', display: 'flex', flexDirection: 'column', gap: 6 }}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, fontSize: 12.5 }}>
                      <span style={{ color: 'var(--text-dim)' }}>Выплата за период</span>
                      <span style={{ color: 'var(--text)', fontWeight: 600 }}>{ru(p.periodStart)} – {ru(p.periodEnd)}</span>
                    </div>
                    <div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, fontSize: 12.5 }}>
                      <span style={{ color: 'var(--text-dim)' }}>За это занятие</span>
                      <span style={{ color: 'var(--text)', fontWeight: 700, fontFamily: 'var(--font-mono)' }}>{new Intl.NumberFormat('ru-RU').format(p.amountRubles)} ₽</span>
                    </div>
                    <div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, fontSize: 12.5 }}>
                      <span style={{ color: 'var(--text-dim)' }}>Статус</span>
                      <span style={{ color: p.status === 'paid' ? 'var(--green)' : 'var(--gold-ink)', fontWeight: 600 }}>{p.status === 'paid' ? ('Выплачено' + (p.payDate ? ' · ' + ru(p.payDate) : '')) : 'Запланирована'}</span>
                    </div>
                  </div>
                )}
                <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5, marginTop: 12 }}>
                  {paidWarn.canConfirm
                    ? 'Удаление занятия НЕ отменит выплату — эти деньги преподавателю уже начислены. Чтобы вернуть их в расчёт, сначала сторнируйте выплату в его карточке.'
                    : (paidWarn.error || 'Занятие уже учтено в выплате преподавателю — обратитесь к администратору.')}
                </div>
                <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
                  <Button variant="ghost" onClick={() => setPaidWarn(null)}>Отмена</Button>
                  {paidWarn.canConfirm && (
                    <Button variant="danger" icon="trash-2" onClick={() => deleteLesson(paidWarn.lesson, true)}>Всё равно удалить</Button>
                  )}
                </div>
              </div>
            </ModalLayer>
          );
        })()}

        {/* Local toast */}
        {toast && (
          <div className="om-fade-in" style={{ position: 'absolute', bottom: 76, left: '50%', transform: 'translateX(-50%)', background: 'var(--text)', color: '#fff', padding: '10px 16px', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', fontSize: 13.5, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap', maxWidth: '88%' }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#6FCF97', flex: 'none' }} /><span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{toast}</span>
          </div>
        )}
      </aside>
    </div>
  );
}

Object.assign(window, { ClientDetail, genClientLessons, ensureClientLessons, shortTeacher });
