// Карточка сотрудника (преподаватель): вкладки «Доступы» и «История действий».
// ТЗ-ПРАВА §1.3, §3.3, §4. Персональные доступы cabinet.* поверх роли «Преподаватель» и журнал
// действий в кабинете. Ручки (все — staff.manage со scope all):
//   GET /staff/:id/permissions → { ok, staffId, role, items: [{ key, description, money, granted, grantedAt, grantedBy }] }
//   PUT /staff/:id/permissions  { grants: { 'cabinet.students_manage': true, … } } → как GET
//   GET /staff/:id/actions?from&to&group&clientId&source&byPermission=1&q&limit&before → { ok, items, next }
// Подписи доступов, групп и источников — константы ниже (сервер отдаёт ключи; description — запасная подпись).
// Вид — как у соседних вкладок StaffCard: Card/Overline/Button/Dropdown/RangeCalendar, таблица th/td,
// строки row-hover om-row-in, скелетоны om-skel.

// ── Справочники ─────────────────────────────────────────────────────────────────────────────────
// Подписи и «можно / нельзя» — ТЗ §1.3; запасные: на экране — тексты каталога сервера (label, can, cannot в GET
// /staff/:id/permissions), они точнее (bagi-F pk-2). Неизвестный ключ без текстов — по description с сервера.
const SA_ACCESS = {
  'cabinet.students_manage': {
    label: 'Ученики: добавлять и править',
    can: 'Добавлять учеников — новый сразу закреплён за преподавателем; править карточку своего ученика: имя, «взрослый», возраст, уровень, контакты ученика, направление.',
    cannot: 'Цену (берётся из настроек школы), статус воронки, менеджера, источник, родителей у готового ученика, архив и открепление, чужих учеников, баланс.',
  },
  'cabinet.schedule_anytime': {
    label: 'Занятия в закрытом периоде',
    can: 'Ставить занятие и серию с датой в закрытом расчётном периоде; переносить своё занятие в закрытый период.',
    cannot: 'Менять время, статус и удалять занятие, которое уже лежит в закрытом периоде (только заявкой, как сейчас); трогать оплаченное; цену и надбавку.',
  },
  'cabinet.students_detach': {
    label: 'Откреплять учеников',
    can: 'Закрыть свою связку с учеником — проведённые занятия остаются в истории.',
    cannot: '',
  },
  'cabinet.tasks_assign': {
    label: 'Поручать задачи коллегам',
    can: 'Ставить исполнителем задачи другого сотрудника.',
    cannot: '',
  },
  'cabinet.schedule_wide': {
    label: 'Закрытый период целиком',
    can: 'Сверх «занятий в закрытом периоде» — править, отмечать и удалять занятия, которые уже лежат в закрытом периоде, кроме оплаченных.',
    cannot: 'Оплаченное, цену.',
  },
};
// Текст подтверждения при включении денежного доступа — ТЗ §4, дословно.
const SA_MONEY_CONFIRM = 'Занятия в прошлом проводятся и списываются с баланса сразу; за уже выплаченный период попадут в выплату как недоплата. Все такие действия видны в истории с пометкой.';

// Группы фильтра «Тип» — значения параметра group (ТЗ §3.3).
const SA_GROUPS = [
  { value: 'lessons', label: 'Занятия' },
  { value: 'events', label: 'События' },
  { value: 'students', label: 'Ученики' },
  { value: 'requests', label: 'Заявки' },
  { value: 'tasks', label: 'Задачи' },
  { value: 'access', label: 'Доступы' },
  { value: 'auth', label: 'Входы' },
];
// Источник действия (staff_actions.source). 'teacher' — кабинет без заголовка X-Artel-Client.
const SA_SOURCES = {
  'teacher-pc': { label: 'Компьютер', icon: 'monitor' },
  'teacher-phone': { label: 'Телефон', icon: 'smartphone' },
  'teacher': { label: 'Кабинет', icon: 'user' },
  'admin-impersonation': { label: 'Администратор в кабинете', icon: 'shield' },
  'crm': { label: 'CRM', icon: 'briefcase' },
};
const SA_SOURCE_OPTS = [
  { value: null, label: 'Все источники' },
  { value: 'teacher-pc', label: 'Компьютер' },
  { value: 'teacher-phone', label: 'Телефон' },
  // Кабинет без пометки устройства (запрос без X-Artel-Client) — строки с такой подписью бывают, отобрать их тоже можно (PK-2).
  { value: 'teacher', label: 'Кабинет' },
  { value: 'admin-impersonation', label: 'Администратор в кабинете' },
  { value: 'crm', label: 'CRM' },
];
// Подписи полей для раскрытия «было → стало» (details.before / details.after).
const SA_FIELDS = {
  scheduledAt: 'Время', durationMinutes: 'Длительность', status: 'Статус', topic: 'Тема', homework: 'ДЗ',
  title: 'Название', name: 'Имя', childName: 'Имя', adult: 'Взрослый', age: 'Возраст', level: 'Уровень',
  phone: 'Телефон', telegram: 'Telegram', email: 'Почта', contacts: 'Контакты', direction: 'Направление',
  subject: 'Направление', timezone: 'Часовой пояс', dueAt: 'Срок', description: 'Описание', notes: 'Описание',
  assignee: 'Исполнитель', permission: 'Доступ', granted: 'Выдан', kind: 'Вид',
  // Ключи, которые пишет сервер (bagi-F pk-1): серия — from/to, задача — assigneeStaffId, ученик — self.
  from: 'Начало серии', to: 'Конец серии', assigneeStaffId: 'Исполнитель', self: 'Взрослый',
  // «Повторять» у существующей записи — PATCH {repeatGroupId} (bagi-G pk-1): номер серии человеку ни о чём не говорит.
  repeatGroupId: 'Серия',
};
const SA_STATUS = {
  scheduled: 'Запланировано', conducted: 'Проведено', missed_student: 'Не пришёл', no_show: 'Не пришёл', noshow: 'Не пришёл',
  cancelled: 'Отменено', cancelled_teacher: 'Отменено', done: 'Выполнена', open: 'Открыта', todo: 'Открыта',
};
// Статусы по сущности строки (сервер пишет сырые значения): задача и ученик — свои слова.
const SA_STATUS_BY = {
  task: { open: 'Открыта', in_progress: 'В работе', done: 'Выполнена', cancelled: 'Отменена' },
  client: { active: 'Занимается', trial: 'Пробный', new: 'Новый', call: 'Звонок', payment: 'Оплата', refusal: 'Отказ' },
};

// ── Помощники ───────────────────────────────────────────────────────────────────────────────────
const SA_PARTS = new Intl.DateTimeFormat('ru-RU', { timeZone: 'Europe/Moscow', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
// «06.10 14:20» по МСК (with year — «06.10.2025 14:20», если год не текущий).
const saWhen = (iso, withYear) => {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d)) return String(iso);
  const p = {}; SA_PARTS.formatToParts(d).forEach((x) => { p[x.type] = x.value; });
  const thisYear = (window.APP_TODAY_ISO || '').slice(0, 4);
  return `${p.day}.${p.month}${withYear || p.year !== thisYear ? '.' + p.year : ''} ${p.hour}:${p.minute}`;
};
const saIso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const saDay = (iso) => new Date(+iso.slice(0, 4), +iso.slice(5, 7) - 1, +iso.slice(8, 10));
const saAddDays = (iso, n) => { const d = saDay(iso); d.setDate(d.getDate() + n); return saIso(d); };
const SA_MON = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
const saRangeLabel = (r) => { const f = (s) => `${+s.slice(8, 10)} ${SA_MON[+s.slice(5, 7) - 1]}`; return r.start === r.end ? f(r.start) : `${f(r.start)} – ${f(r.end)}`; };
// Значение поля для «было → стало»: даты по МСК, булевы словами, статусы по-русски.
const saVal = (k, v, entity) => {
  if (k === 'repeatGroupId') return v ? 'привязана к серии' : 'без серии';
  if (v == null || v === '') return '—';
  if (typeof v === 'boolean') return v ? 'да' : 'нет';
  if (Array.isArray(v)) return v.map((x) => saVal(k, x, entity)).join(', ') || '—';
  if (typeof v === 'object') return v.value != null ? String(v.value) : JSON.stringify(v);
  if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(v)) return saWhen(v);
  if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v.slice(8, 10) + '.' + v.slice(5, 7) + '.' + v.slice(0, 4);
  if (k === 'status' && SA_STATUS_BY[entity] && SA_STATUS_BY[entity][v]) return SA_STATUS_BY[entity][v];
  if (k === 'status' && SA_STATUS[v]) return SA_STATUS[v];
  if (k === 'assigneeStaffId') {
    const s = ((window.CRM_DATA && window.CRM_DATA.STAFF) || []).find((x) => String(x.id) === String(v));
    return s && s.name ? s.name : '#' + v;
  }
  if (k === 'durationMinutes') return v + ' мин';
  if (k === 'permission' && SA_ACCESS[v]) return SA_ACCESS[v].label;
  return String(v);
};
const saErr = (e) => (e && e.message) || 'не удалось загрузить';
const saBar = (w, h = 12) => <span className="om-skel" style={{ display: 'block', width: w, height: h, borderRadius: 4, background: 'var(--bg-soft)' }} />;

// Пилюля-пометка: «деньги», «по доступу» (янтарные), «администратор» (бренд).
function SAPill({ tone = 'amber', icon, children, title }) {
  const c = tone === 'brand' ? { bg: 'var(--brand-soft)', fg: 'var(--brand-ink)' } : { bg: 'var(--amber-bg)', fg: 'var(--amber-ink)' };
  return (
    <span title={title} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 8px', borderRadius: 999, background: c.bg, color: c.fg, fontSize: 11.5, fontWeight: 700, whiteSpace: 'nowrap', lineHeight: 1.5 }}>
      {icon && <Icon name={icon} size={12} strokeWidth={2.2} />}{children}
    </span>
  );
}

// Состояние «нельзя»: 403 — не администратор; иначе текст сервера и «Повторить».
function SAProblem({ forbidden, text, error, onRetry }) {
  return (
    <Card pad={0} style={{ overflow: 'hidden' }}>
      <div className="sa-problem" style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '18px 18px' }}>
        <Icon name={forbidden ? 'lock' : 'alert-circle'} size={18} style={{ color: forbidden ? 'var(--text-dim)' : 'var(--coral)', flex: 'none', marginTop: 1 }} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{forbidden ? text : 'Не удалось загрузить'}</div>
          <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, lineHeight: 1.5 }}>
            {forbidden ? 'Нужно право «Управлять сотрудниками» — оно есть у администратора.' : error}
          </div>
        </div>
        {!forbidden && <Button variant="secondary" size="sm" icon="rotate-ccw" onClick={onRetry}>Повторить</Button>}
      </div>
    </Card>
  );
}

// ── Вкладка «Доступы» ───────────────────────────────────────────────────────────────────────────
function StaffAccessTab({ staff, onToast, onHistory }) {
  const toast = onToast || (() => {});
  const [items, setItems] = React.useState(null);   // null — грузим; [] — пусто
  const [draft, setDraft] = React.useState({});     // key → bool (что на экране)
  const [loadErr, setLoadErr] = React.useState(null); // { forbidden, text }
  const [readOnly, setReadOnly] = React.useState(false); // PUT ответил 403 — тумблеры замкнуты
  const [busy, setBusy] = React.useState(false);
  const [saveErr, setSaveErr] = React.useState('');
  const [recent, setRecent] = React.useState(null);  // последние изменения доступа (3 строки)
  const [ver, setVer] = React.useState(0);
  const base = '/staff/' + staff.id;

  const take = (r) => {
    const list = Array.isArray(r && r.items) ? r.items : [];
    setItems(list);
    const d = {}; list.forEach((it) => { d[it.key] = !!it.granted; }); setDraft(d);
  };
  React.useEffect(() => {
    if (!window.apiCall) return;
    let live = true;
    setItems(null); setLoadErr(null);
    window.apiCall(base + '/permissions')
      .then((r) => { if (live) take(r); })
      .catch((e) => { if (live) setLoadErr({ forbidden: e.status === 403, text: saErr(e) }); });
    return () => { live = false; };
  }, [staff.id, ver]);
  const loadRecent = React.useCallback(() => {
    if (!window.apiCall) return;
    // Без дат сервер берёт 30 дней, и выдача постарше показывалась как «Доступы ещё не выдавались» (bagi-F prava-2):
    // берём всю историю — с начала по завтра (запас на часовой пояс браузера против МСК).
    window.apiCall(base + '/actions?group=access&limit=3&from=2000-01-01&to=' + saAddDays(saIso(new Date()), 1))
      .then((r) => setRecent(Array.isArray(r && r.items) ? r.items : []))
      .catch(() => setRecent(null));
  }, [staff.id]);
  React.useEffect(() => { loadRecent(); }, [loadRecent, ver]);

  const changed = (items || []).filter((it) => !!it.granted !== !!draft[it.key]);
  const изменено = changed.length > 0;

  const toggle = async (it, on) => {
    if (readOnly || busy) return;
    // Денежный доступ — только через подтверждение с последствиями (ТЗ §4).
    if (on && it.money && !it.granted && window.crmConfirm) {
      const ok = await window.crmConfirm({ title: `Выдать «${(SA_ACCESS[it.key] || {}).label || it.description}»?`, body: SA_MONEY_CONFIRM, ok: 'Выдать доступ' });
      if (!ok) return;
    }
    setSaveErr('');
    setDraft((d) => ({ ...d, [it.key]: on }));
  };
  const cancel = () => { const d = {}; (items || []).forEach((it) => { d[it.key] = !!it.granted; }); setDraft(d); setSaveErr(''); };
  const save = async () => {
    if (!изменено || busy) return;
    // Тело — полное состояние всех пунктов каталога: сервер пишет в журнал только изменившиеся.
    const grants = {}; (items || []).forEach((it) => { grants[it.key] = !!draft[it.key]; });
    setBusy(true); setSaveErr('');
    try {
      // Денежный ключ включается только с confirmMoney:true (заявка R-B: без флага — 409 confirm_required).
      const денежныйВкл = changed.some((it) => it.money && !it.granted && !!draft[it.key]);
      const тело = денежныйВкл ? { grants, confirmMoney: true } : { grants };
      let r;
      try {
        r = await window.apiCall(base + '/permissions', { method: 'PUT', body: JSON.stringify(тело) });
      } catch (e) {
        const код = e && (e.code || (e.payload && e.payload.code) || (e.data && e.data.code));
        if (e.status !== 409 || код !== 'confirm_required' || !window.crmConfirm) throw e;
        const да = await window.crmConfirm({ title: 'Выдать доступ с деньгами?', body: saErr(e) || SA_MONEY_CONFIRM, ok: 'Выдать доступ' });
        if (!да) return; // busy снимет finally
        r = await window.apiCall(base + '/permissions', { method: 'PUT', body: JSON.stringify({ grants, confirmMoney: true }) });
      }
      take(r);
      toast(changed.length === 1 ? 'Доступ сохранён — действует сразу' : 'Доступы сохранены — действуют сразу');
      loadRecent();
    } catch (e) {
      if (e.status === 403) { setReadOnly(true); cancel(); setSaveErr('Доступы меняет администратор.'); }
      else setSaveErr(saErr(e));
    } finally { setBusy(false); }
  };

  if (loadErr) return <div style={{ maxWidth: 820 }}><SAProblem forbidden={loadErr.forbidden} text="Доступы меняет администратор" error={loadErr.text} onRetry={() => setVer((v) => v + 1)} /></div>;

  return (
    <div className="sa-access" style={{ maxWidth: 820, display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
        <Overline>Доступы в кабинете</Overline>
        <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>сверх роли «Преподаватель» · по умолчанию всё выключено</span>
      </div>

      {items === null ? [0, 1].map((i) => (
        <Card key={i} pad={18}>
          <div style={{ display: 'flex', gap: 12 }}>
            <span className="om-skel" style={{ width: 16, height: 16, borderRadius: 4, background: 'var(--bg-soft)', flex: 'none' }} />
            <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 9 }}>{saBar('38%', 14)}{saBar('86%')}{saBar('72%')}</div>
          </div>
        </Card>
      )) : items.length === 0 ? (
        <Card pad={18}><div style={{ fontSize: 13.5, color: 'var(--text-dim)' }}>Персональных доступов в каталоге пока нет.</div></Card>
      ) : items.map((it) => {
        // Тексты — из каталога сервера (КАТАЛОГ_ДОСТУПОВ: там граница «не раньше прошлого периода» и письма
        // администраторам, решение В1); SA_ACCESS — запасной, если сервер их не прислал (bagi-F pk-2).
        const local = SA_ACCESS[it.key] || {};
        const meta = { label: it.label || local.label || it.description || it.key,
          can: it.can || local.can || '', cannot: it.cannot || local.cannot || '' };
        const on = !!draft[it.key];
        const diff = on !== !!it.granted;
        const by = it.grantedBy && (it.grantedBy.login || ('#' + it.grantedBy.userId));
        return (
          <Card key={it.key} pad={0} className="sa-item om-card-in" style={{ overflow: 'hidden', borderColor: diff ? 'var(--brand)' : 'var(--border)' /* не undefined: React снял бы цвет из border и рамка стала бы чёрной */ }}>
            <label data-key={it.key} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '16px 18px', cursor: readOnly ? 'default' : 'pointer' }}>
              <input type="checkbox" checked={on} disabled={readOnly || busy} onChange={(e) => toggle(it, e.target.checked)}
                style={{ width: 16, height: 16, marginTop: 2, accentColor: 'var(--brand)', flex: 'none', cursor: readOnly ? 'default' : 'pointer' }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontSize: 14.5, fontWeight: 700, color: 'var(--text)' }}>{meta.label}</span>
                  {it.money && <SAPill icon="wallet" title="Задевает деньги: проведение прошлых занятий и выплаты">деньги</SAPill>}
                </div>
                {meta.can && <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 6, lineHeight: 1.5 }}><b style={{ color: 'var(--green-ink, var(--green))', fontWeight: 600 }}>Можно: </b>{meta.can}</div>}
                {meta.cannot && <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, lineHeight: 1.5 }}><b style={{ color: 'var(--text-dim)', fontWeight: 600 }}>Нельзя: </b>{meta.cannot}</div>}
              </div>
              <div style={{ flex: 'none', textAlign: 'right', fontSize: 12.5, lineHeight: 1.45, minWidth: 150, paddingTop: 1 }}>
                {diff ? <span style={{ color: 'var(--gold-ink)', fontWeight: 600 }}>{on ? 'Будет выдан' : 'Будет снят'} после сохранения</span>
                  : it.granted ? <span style={{ color: 'var(--text-secondary)' }}>{by ? 'Выдал ' : 'Выдан'}<b style={{ color: 'var(--text)', fontWeight: 600 }}>{by || ''}</b>{it.grantedAt ? ' · ' + saWhen(it.grantedAt) : ''}</span>
                  : <span style={{ color: 'var(--text-dim)' }}>Выключено</span>}
              </div>
            </label>
          </Card>
        );
      })}

      {(изменено || saveErr) && (
        <div className="sa-save om-fade-in" style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
          {изменено && <Button variant="primary" icon="check" onClick={save}>{busy ? 'Сохраняем…' : 'Сохранить'}</Button>}
          {изменено && <Button variant="ghost" onClick={cancel}>Отменить</Button>}
          {изменено && !saveErr && <span style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>Изменения действуют сразу после сохранения</span>}
          {saveErr && <span className="sa-save-err" style={{ fontSize: 12.5, color: readOnly ? 'var(--text-secondary)' : 'var(--coral)' }}>{saveErr}</span>}
        </div>
      )}

      {/* Последние изменения доступа — три строки журнала group=access. */}
      <Card pad={0} style={{ overflow: 'hidden', marginTop: 8 }}>
        <div style={{ padding: '14px 16px 10px', display: 'flex', alignItems: 'center', gap: 10 }}>
          <Overline>Последние изменения доступа</Overline>
          <div style={{ flex: 1 }} />
          {onHistory && <Button variant="ghost" size="sm" onClick={onHistory}>Вся история</Button>}
        </div>
        <div className="sa-recent">
          {recent === null ? <div style={{ padding: '4px 16px 14px', fontSize: 13, color: 'var(--text-dim)' }}>—</div>
            : recent.length === 0 ? <div style={{ padding: '4px 16px 14px', fontSize: 13, color: 'var(--text-dim)' }}>Доступы ещё не выдавались.</div>
            : recent.map((a) => (
              <div key={a.id} style={{ display: 'flex', gap: 14, padding: '10px 16px', borderTop: '1px solid var(--border)', fontSize: 13.5 }}>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--text-dim)', whiteSpace: 'nowrap', paddingTop: 1 }}>{saWhen(a.at)}</span>
                <span style={{ color: 'var(--text)' }}>{a.summary}</span>
              </div>
            ))}
        </div>
      </Card>

      <div style={{ display: 'flex', gap: 9, alignItems: 'flex-start', padding: '11px 13px', background: 'var(--bg-soft)', borderRadius: 'var(--radius-sm)' }}>
        <Icon name="info" size={15} style={{ color: 'var(--text-dim)', flex: 'none', marginTop: 1 }} />
        <div style={{ fontSize: 12.5, lineHeight: 1.5, color: 'var(--text-secondary)' }}>
          Остальное — по роли «Преподаватель»: свои занятия, свои ученики (просмотр), задачи себе. Изменения действуют сразу.
        </div>
      </div>
    </div>
  );
}

// ── Вкладка «История действий» ──────────────────────────────────────────────────────────────────
const SA_PAGE = 50;
const SA_ORDER = ['name', 'title', 'from', 'to', 'scheduledAt', 'durationMinutes', 'status'];

function SADetails({ a }) {
  const d = a.details || {};
  // Выдача и снятие доступа: before/after — булевы (выдан или нет), сам доступ — в d.permission (bagi-F pk-1).
  const flag = typeof d.before === 'boolean' || typeof d.after === 'boolean';
  const obj = (x) => (x && typeof x === 'object' && !Array.isArray(x) ? x : {});
  const before = flag ? (typeof d.before === 'boolean' ? { granted: d.before } : {}) : obj(d.before);
  let after = flag ? (typeof d.after === 'boolean' ? { granted: d.after } : {}) : obj(d.after);
  // Заявка администратору (bagi-G pk-2): сервер пишет null в after у полей, которые заявка не меняет, — это не «стёрто».
  // Перенос и отметка показывают только меняемое; удаление — прежние значения без стрелок.
  if (a.entity === 'move_request') after = d.kind === 'delete' ? {} : Object.fromEntries(Object.entries(after).filter(([, v]) => v != null));
  const неизменные = a.entity === 'move_request' && d.kind !== 'delete' ? new Set(Object.keys(before).filter((k) => !(k in after))) : new Set();
  // jsonb хранит ключи по длине («to» раньше «from») — ставим привычный порядок, прочие следом как пришли.
  const rank = (k) => { const i = SA_ORDER.indexOf(k); return i < 0 ? SA_ORDER.length : i; };
  const keys = Array.from(new Set([...Object.keys(before), ...Object.keys(after)])).filter((k) => !неизменные.has(k)).sort((x, y) => rank(x) - rank(y));
  const actor = a.actor || {};
  const who = actor.name || actor.login || (actor.userId != null ? '#' + actor.userId : null);
  return (
    <div className="sa-details" style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
      {d.permission && (
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ color: 'var(--text-dim)', minWidth: 110 }}>{SA_FIELDS.permission}</span>
          <b style={{ color: 'var(--text)' }}>{saVal('permission', d.permission)}</b>
          {d.money === true && <span style={{ color: 'var(--amber-ink)' }}>· деньги</span>}
        </div>
      )}
      {keys.length > 0 ? keys.map((k) => (
        <div key={k} style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ color: 'var(--text-dim)', minWidth: 110 }}>{SA_FIELDS[k] || k}</span>
          {k in before && <span style={{ textDecoration: 'line-through', color: 'var(--text-dim)' }}>{saVal(k, before[k], a.entity)}</span>}
          {k in before && k in after && <span>→</span>}
          {k in after && <b style={{ color: 'var(--text)' }}>{saVal(k, after[k], a.entity)}</b>}
        </div>
      )) : Array.isArray(d.fields) && d.fields.length > 0 ? (
        <div>Изменено: {d.fields.map((f) => SA_FIELDS[f] || f).join(', ')}</div>
      ) : <div style={{ color: 'var(--text-dim)' }}>Подробностей нет.</div>}
      {a.entity === 'move_request' && d.reason && <div>Причина: <b style={{ color: 'var(--text)' }}>{String(d.reason)}</b></div>}
      {d.count != null && <div>Занятий в серии: <b style={{ color: 'var(--text)' }}>{d.count}</b></div>}
      {d.lockedBefore && <div>Закрытый период — до <b style={{ color: 'var(--text)' }}>{saVal('lockedBefore', d.lockedBefore)}</b></div>}
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', marginTop: 2, fontSize: 12.5, color: 'var(--text-dim)' }}>
        {who && <span>Нажал: <span style={{ color: 'var(--text-secondary)', fontWeight: 600 }}>{who}</span>{actor.admin ? ' (администратор)' : ''}{actor.login && actor.name && String(actor.login).trim() !== String(actor.name).trim() ? ' · ' + actor.login : ''}</span>}
        <span>{saWhen(a.at, true)} МСК</span>
        {a.requestId && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>запрос {a.requestId}</span>}
      </div>
    </div>
  );
}

function StaffHistoryTab({ staff }) {
  const today = window.APP_TODAY_ISO || saIso(new Date());
  const [range, setRange] = React.useState({ start: saAddDays(today, -29), end: today });
  const [calOpen, setCalOpen] = React.useState(false);
  const [groups, setGroups] = React.useState([]);
  const [clientId, setClientId] = React.useState(null);
  const [source, setSource] = React.useState(null);
  const [byPerm, setByPerm] = React.useState(false);
  const [qInput, setQInput] = React.useState('');
  const [q, setQ] = React.useState('');
  React.useEffect(() => { const t = setTimeout(() => setQ(qInput.trim().slice(0, 100)), 300); return () => clearTimeout(t); }, [qInput]);

  const [rows, setRows] = React.useState(null);  // null — грузим первую страницу
  const [next, setNext] = React.useState(null);
  const [err, setErr] = React.useState(null);    // { forbidden, text }
  const [more, setMore] = React.useState(false); // догружаем «Показать ещё»
  const [moreErr, setMoreErr] = React.useState('');
  const [open, setOpen] = React.useState(null);  // id раскрытой строки
  const [ver, setVer] = React.useState(0);
  const seq = React.useRef(0);

  // Ученики преподавателя — для фильтра «Ученик».
  const [students, setStudents] = React.useState([]);
  React.useEffect(() => {
    if (!window.apiCall) return;
    window.apiCall('/teachers/' + staff.id + '/students')
      .then((r) => setStudents(Array.isArray(r && r.students) ? r.students : []))
      .catch(() => setStudents([]));
  }, [staff.id]);
  const studentOpts = [{ value: null, label: 'Все ученики' }, ...students.map((s) => ({ value: s.id, label: s.name || s.childName || ('#' + s.id) }))];

  const query = (before) => {
    const p = new URLSearchParams();
    p.set('from', range.start); p.set('to', range.end);
    if (groups.length) p.set('group', SA_GROUPS.map((g) => g.value).filter((g) => groups.includes(g)).join(','));
    if (clientId != null) p.set('clientId', String(clientId));
    if (source) p.set('source', source);
    if (byPerm) p.set('byPermission', '1');
    if (q) p.set('q', q);
    p.set('limit', String(SA_PAGE));
    if (before != null) p.set('before', String(before));
    return '/staff/' + staff.id + '/actions?' + p.toString();
  };

  React.useEffect(() => {
    if (!window.apiCall) return;
    const my = ++seq.current;
    // setMore(false): догрузка прошлого фильтра больше не наша — её .finally сброс пропустит (PK-1: кнопка залипала «Загружаем…»).
    setRows(null); setErr(null); setNext(null); setOpen(null); setMoreErr(''); setMore(false);
    window.apiCall(query(null))
      .then((r) => { if (my !== seq.current) return; setRows(Array.isArray(r && r.items) ? r.items : []); setNext(r && r.next != null ? r.next : null); })
      .catch((e) => { if (my !== seq.current) return; setErr({ forbidden: e.status === 403, text: saErr(e) }); setRows([]); });
  }, [staff.id, range.start, range.end, groups.join(','), clientId, source, byPerm, q, ver]);

  const loadMore = () => {
    if (next == null || more) return;
    const my = seq.current;
    setMore(true); setMoreErr('');
    window.apiCall(query(next))
      .then((r) => { if (my !== seq.current) return; setRows((x) => [...(x || []), ...(Array.isArray(r && r.items) ? r.items : [])]); setNext(r && r.next != null ? r.next : null); })
      .catch((e) => { if (my === seq.current) setMoreErr(saErr(e)); })
      .finally(() => { if (my === seq.current) setMore(false); });
  };

  // Стрелки двигают окно на его же длину (30 дней → предыдущие 30 дней).
  const shift = (dir) => {
    if (dir > 0 && range.end >= today) return; // в будущее листать незачем
    const len = Math.round((saDay(range.end) - saDay(range.start)) / 86400000) + 1;
    setRange({ start: saAddDays(range.start, dir * len), end: saAddDays(range.end, dir * len) });
  };
  const filtered = groups.length > 0 || clientId != null || source || byPerm || q;
  const reset = () => { setGroups([]); setClientId(null); setSource(null); setByPerm(false); setQInput(''); setQ(''); };

  const th = { textAlign: 'left', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--text-dim)', padding: '0 16px 12px', whiteSpace: 'nowrap' };
  const td = { padding: '12px 16px', borderTop: '1px solid var(--border)', fontSize: 14, color: 'var(--text)', verticalAlign: 'top' };
  const navBtn = { width: 34, height: 34, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)', transition: 'background var(--dur-fast), border-color var(--dur-fast)' };
  const hov = (e) => { e.currentTarget.style.background = 'var(--bg-soft)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; };
  const out = (e) => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border)'; };

  if (err && err.forbidden) return <div style={{ maxWidth: 820 }}><SAProblem forbidden text="Историю действий видит администратор" /></div>;

  return (
    <div className="sa-history" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
      {/* Фильтры — полосой, как у «Занятий». */}
      <div className="sa-filters" style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
          <button title="Раньше" className="sa-prev" onClick={() => shift(-1)} onMouseEnter={hov} onMouseLeave={out} style={navBtn}><Icon name="chevron-left" size={16} /></button>
          <div style={{ position: 'relative' }}>
            <button className="sa-range" onClick={() => setCalOpen((o) => !o)} onMouseEnter={hov} onMouseLeave={out}
              style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 12px', cursor: 'pointer', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, color: 'var(--text)', whiteSpace: 'nowrap', transition: 'background var(--dur-fast), border-color var(--dur-fast)' }}>
              <Icon name="calendar" size={15} style={{ color: 'var(--text-dim)' }} />{saRangeLabel(range)}
            </button>
            {calOpen && window.RangeCalendar && <RangeCalendar value={range} maxISO={today} onApply={(r) => { if (r && r.start && r.end) setRange({ start: r.start, end: r.end }); }} onClose={() => setCalOpen(false)} />}
          </div>
          <button title="Позже" className="sa-next" onClick={() => shift(1)} onMouseEnter={hov} onMouseLeave={out} style={{ ...navBtn, opacity: range.end >= today ? 0.45 : 1, cursor: range.end >= today ? 'default' : 'pointer' }}><Icon name="chevron-right" size={16} /></button>
        </div>
        <div className="sa-f-group"><Dropdown value={groups} onChange={setGroups} options={SA_GROUPS} placeholder="Тип" width={150} multi searchable={false} /></div>
        <div className="sa-f-client"><Dropdown value={clientId} onChange={setClientId} options={studentOpts} placeholder="Все ученики" width={184} searchable /></div>
        <div className="sa-f-source"><Dropdown value={source} onChange={setSource} options={SA_SOURCE_OPTS} placeholder="Все источники" width={200} searchable={false} /></div>
        <button className="sa-f-perm" onClick={() => setByPerm((v) => !v)} aria-pressed={byPerm}
          style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 12px', cursor: 'pointer', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap', transition: 'background var(--dur-fast), border-color var(--dur-fast)',
            background: byPerm ? 'var(--amber-bg)' : 'var(--bg-card)', color: byPerm ? 'var(--amber-ink)' : 'var(--text-secondary)', border: '1px solid ' + (byPerm ? '#E2D3A8' : 'var(--border)') }}>
          <Icon name="key-round" size={14} />Только по доступу
        </button>
        <div className="sa-f-q" style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', padding: '8px 12px', width: 230 }}>
          <Icon name="search" size={16} style={{ color: 'var(--text-dim)' }} />
          <input value={qInput} maxLength={100} onChange={(e) => setQInput(e.target.value)} placeholder="Найти в описании…" style={{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, fontFamily: 'var(--font-sans)', fontSize: 13.5, color: 'var(--text)' }} />
        </div>
        {filtered && <Button variant="ghost" size="sm" onClick={reset}>Сбросить</Button>}
      </div>

      <Card pad={0} style={{ overflow: 'hidden' }}>
        <div style={{ padding: '16px 16px 12px', display: 'flex', alignItems: 'center', gap: 10 }}>
          <Overline>История действий</Overline>
          <span className="sa-count" style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-dim)' }}>{rows ? rows.length + (next != null ? '+' : '') : '…'} · {saRangeLabel(range)}</span>
          <div style={{ flex: 1 }} />
          <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>время — МСК</span>
        </div>
        {err ? (
          <div className="sa-error" style={{ padding: '18px 16px 20px', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 12 }}>
            <Icon name="alert-circle" size={16} style={{ color: 'var(--coral)', flex: 'none' }} />
            <span style={{ fontSize: 13.5, color: 'var(--text-secondary)', flex: 1 }}>Не удалось загрузить историю: {err.text}</span>
            <Button variant="secondary" size="sm" icon="rotate-ccw" onClick={() => setVer((v) => v + 1)}>Повторить</Button>
          </div>
        ) : rows === null ? (
          <div className="sa-skel">{[0, 1, 2, 3, 4].map((i) => (
            <div key={i} style={{ display: 'grid', gridTemplateColumns: '110px 1fr 160px 150px', gap: 16, padding: '14px 16px', borderTop: '1px solid var(--border)' }}>
              {saBar('80%')}{saBar(['70%', '55%', '82%', '60%', '48%'][i])}{saBar('70%')}{saBar('60%')}
            </div>
          ))}</div>
        ) : rows.length === 0 ? (
          <div className="sa-empty" style={{ padding: '24px 16px', borderTop: '1px solid var(--border)', color: 'var(--text-dim)', fontSize: 14 }}>
            {filtered ? 'По фильтру за выбранный период действий нет.' : 'За выбранный период действий нет.'}
          </div>
        ) : (
          <table className="sa-table" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
            <colgroup><col style={{ width: 118 }} /><col /><col style={{ width: 180 }} /><col style={{ width: 200 }} /></colgroup>
            <thead><tr><th style={th}>Когда</th><th style={th}>Что сделано</th><th style={th}>Ученик</th><th style={th}>Источник</th></tr></thead>
            <tbody>
              {rows.map((a, i) => {
                const src = SA_SOURCES[a.source] || { label: a.source || '—', icon: 'circle' };
                const isOpen = open === a.id;
                const admin = a.actor && a.actor.admin;
                return (
                  <React.Fragment key={a.id}>
                    <tr className="row-hover om-row-in sa-row" data-id={a.id} onClick={() => setOpen(isOpen ? null : a.id)} style={{ animationDelay: Math.min((i % SA_PAGE) * 18, 280) + 'ms', cursor: 'pointer', background: isOpen ? 'var(--bg-soft)' : undefined }}>
                      <td style={{ ...td, fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text-secondary)', whiteSpace: 'nowrap' }}>{saWhen(a.at)}</td>
                      <td style={td}>
                        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
                          <Icon name={isOpen ? 'chevron-down' : 'chevron-right'} size={14} style={{ color: 'var(--text-dim)', flex: 'none', marginTop: 3 }} />
                          <div style={{ minWidth: 0 }}>
                            <span style={{ lineHeight: 1.45 }}>{a.summary}</span>
                            {(a.byPermission || admin) && (
                              <span style={{ display: 'inline-flex', gap: 6, marginLeft: 8, verticalAlign: 'middle' }}>
                                {a.byPermission && <SAPill icon="key-round" title={'Прошло благодаря доступу «' + ((SA_ACCESS[a.byPermission] || {}).label || a.byPermission) + '»'}>по доступу</SAPill>}
                                {admin && <SAPill tone="brand" icon="shield" title="Действие сделал администратор">администратор</SAPill>}
                              </span>
                            )}
                          </div>
                        </div>
                      </td>
                      <td style={{ ...td, color: a.clientName ? 'var(--text)' : 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.clientName || '—'}</td>
                      <td style={{ ...td, color: 'var(--text-secondary)', fontSize: 13.5 }}>
                        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><Icon name={src.icon} size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />{src.label}</span>
                      </td>
                    </tr>
                    {isOpen && (
                      <tr className="sa-open"><td colSpan={4} style={{ padding: '4px 16px 16px 50px', background: 'var(--bg-soft)' }}><SADetails a={a} /></td></tr>
                    )}
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        )}
        {rows && rows.length > 0 && (next != null || moreErr) && (
          <div style={{ padding: '12px 16px 16px', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 12 }}>
            {next != null && <Button variant="secondary" size="sm" icon={more ? 'loader' : 'chevron-down'} onClick={loadMore}>{more ? 'Загружаем…' : 'Показать ещё'}</Button>}
            {moreErr && <span style={{ fontSize: 12.5, color: 'var(--coral)' }}>{moreErr}</span>}
          </div>
        )}
      </Card>
    </div>
  );
}

Object.assign(window, { StaffAccessTab, StaffHistoryTab });
