/* ctor-type-tabs.jsx — «Работы» → вкладки типов помещения (спека 2026-10-12-ctor-works-merge §3, §5, очередь 2):
   полоса вкладок (WorksTypeBar) и вкладка типа (WorksTypeTab) — шапка с правилом подбора, матрица состава
   «работа × тип ремонта» (window.CtorTypeMatrix). Правило типа объекта правится нынешним редактором (TaxObjectDrawer,
   ctor-template-axes.jsx), свойства типа ремонта — редактором шаблона (PresetDrawer, ctor-presets.jsx).
   На «Квартире» клик по ячейке меняет состав самого типа ремонта (excludeSlugs) — для всех вкладок; на остальных
   вкладках — исключение пары «тип помещения × тип ремонта» (ConstructorData.saveTypeProfiles).
   У каждой вкладки типа (кроме «Все работы») при праве правки — меню «⋯»: переименовать, редактировать (тот же редактор «Правило и этапы»),
   удалить; правка идёт через тот же setRules (правила типа объекта страницы), что и «Правило и этапы».
   У колонки типа ремонта (шапка «Состава», при праве правки) — свой «⋯»: «Свойства» и «Удалить»; стандартные типы компании не удаляются. */

const ttSt = {
  bar: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, margin: '0 4px 12px' },
  tabs: { display: 'flex', flexWrap: 'wrap', gap: 4 },
  count: { marginLeft: 6, padding: '0 6px', borderRadius: 980, fontSize: 11, fontWeight: 700, background: 'var(--secondary)', color: 'var(--text-secondary)' },
  head: { background: 'var(--card, #fff)', border: '1px solid var(--border)', borderRadius: 14, padding: '12px 16px', margin: '0 4px 12px' },
  headRow: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' },
  name: { fontSize: 16, fontWeight: 600 },
  muted: { fontSize: 12.5, color: 'var(--text-secondary)' },
  note: { fontSize: 12.5, color: 'var(--text-secondary)', marginTop: 8 },
  warn: { fontSize: 12.5, color: 'var(--warning-strong, #92620a)', marginTop: 6 },
  mergeBtn: { border: 0, background: 'none', padding: 0, marginLeft: 10, font: 'inherit', color: 'inherit', cursor: 'pointer', textDecoration: 'underline dotted', textUnderlineOffset: 3 },
  mergeList: { margin: '4px 0 0', paddingLeft: 18 },
  tools: { display: 'flex', gap: 6, flexWrap: 'wrap' },
  btn: { height: 30, padding: '0 10px', borderRadius: 8, border: '1px solid var(--border)', background: 'var(--card, #fff)', fontSize: 12.5, cursor: 'pointer', fontFamily: 'inherit' },
  danger: { color: 'var(--destructive, #c0392b)' },
  filters: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', margin: '0 4px 10px' },
  input: { height: 34, padding: '0 10px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 13, minWidth: 220, background: 'var(--card, #fff)' },
  select: { height: 34, padding: '0 8px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 13, background: 'var(--card, #fff)' },
  // колонок столько же, сколько типов ремонта: уже ~760 px — прокрутка по горизонтали
  scroll: { overflowX: 'auto' },
  table: { width: '100%', minWidth: 760, borderCollapse: 'collapse', tableLayout: 'fixed', fontSize: 13 },
  th: { textAlign: 'center', fontSize: 11.5, fontWeight: 600, color: 'var(--text-secondary)', padding: 8, borderBottom: '1px solid var(--border)' },
  thBtn: { border: 0, background: 'none', padding: 0, font: 'inherit', color: 'inherit', cursor: 'pointer', textDecoration: 'underline dotted', textUnderlineOffset: 3 },
  td: { padding: '5px 6px', borderBottom: '1px solid var(--border)', textAlign: 'center', verticalAlign: 'middle' },
  tdName: { padding: '5px 8px', borderBottom: '1px solid var(--border)', verticalAlign: 'middle' },
  stage: { padding: '9px 8px', background: 'var(--secondary)', fontWeight: 600, fontSize: 13 },
  workBtn: { border: 0, background: 'none', padding: 0, font: 'inherit', color: 'var(--foreground)', textAlign: 'left', cursor: 'pointer' },
  cell: { position: 'relative', width: 34, height: 26, borderRadius: 7, border: '1px solid var(--border)', background: 'var(--card, #fff)', fontSize: 14, lineHeight: '24px', cursor: 'pointer', fontFamily: 'inherit', color: 'var(--foreground)' },
  cellOff: { color: 'var(--text-tertiary, #b4b2a9)' },
  cellLocked: { cursor: 'not-allowed', opacity: 0.55 },
  dot: { position: 'absolute', top: 3, right: 3, width: 6, height: 6, borderRadius: '50%', background: 'var(--accent-strong, #2563eb)' },
  reset: { border: 0, background: 'none', cursor: 'pointer', color: 'var(--text-secondary)', fontSize: 13, padding: '0 4px' },
  empty: { padding: '28px 8px', textAlign: 'center', color: 'var(--text-secondary)' },
  // вкладка типа + «⋯»: подложка у обёртки, чтобы выбранная вкладка и её кнопка были одной плашкой
  tabWrap: { position: 'relative', display: 'inline-flex', alignItems: 'center', borderRadius: 7 },
  dots: { padding: '4px 6px', border: 'none', borderRadius: 7, background: 'transparent', color: 'var(--text-secondary)', fontSize: 14, lineHeight: 1, cursor: 'pointer', fontFamily: 'inherit' },
  menu: { position: 'absolute', top: 'calc(100% + 4px)', left: 0, zIndex: 50, minWidth: 190, padding: 4, display: 'flex', flexDirection: 'column', background: 'var(--card, #fff)', border: '1px solid var(--border-subtle)', borderRadius: 10, boxShadow: 'var(--shadow-pop)' },
  menuItem: { padding: '7px 10px', border: 'none', borderRadius: 7, background: 'transparent', fontSize: 13, textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit', color: 'var(--foreground)' },
  renameInput: { height: 28, width: 170, padding: '0 8px', borderRadius: 7, border: '1px solid var(--border)', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', background: 'var(--card, #fff)', color: 'var(--foreground)' },
};

/** тип помещения можно удалить, кроме основного («Квартира»): на нём правится общий состав типов ремонта, он же — тип по умолчанию */
const ttCanDelete = (ruleId) => ruleId !== window.CtorTypeMatrix.BASE_TYPE_ID;
/** удалить тип помещения: правило уходит из списка, исключения, цены и разметка вкладки — из профилей (сметы этого типа вернутся к библиотеке) */
function ttDropType(ruleId, setRules, setProfiles) {
  setRules((list) => list.filter((x) => x.id !== ruleId));
  setProfiles((p) => { const next = { ...p }; delete next[ruleId]; return next; });
}
const TT_LOCK = {
  phase: 'фаза не входит в тип ремонта — включается в его свойствах (заголовок колонки)',
  stage: 'этап исключён из типа ремонта целиком — включается в его свойствах (заголовок колонки)',
  object: 'этап «Типа объекта» (дом, коммерция) — его включает правило вкладки, а не тип ремонта',
  rule: 'работу включает или выключает правило вкладки — меняется в «Правило и этапы»',
  slug: 'исключено общим кодом работы в свойствах типа ремонта (поле «Исключения»)',
};

/** полоса вкладок: «Все работы» (если есть право каталога) + типы помещения в порядке подбора; «+ тип помещения» — рядом.
    С правом правки (canEdit) у каждой вкладки типа — «⋯»: «Переименовать» (название правится на месте), «Редактировать» (редактор «Правило и этапы»
    для этого типа) и «Удалить» (с подтверждением; основной тип «Квартира» не удаляется). Правки идут через setRules / setProfiles страницы —
    те же, что у вкладки типа; удалённая открытая вкладка → onDeleted(id) (страница уходит на «Все работы») */
function WorksTypeBar({ rules, value, onPick, onAdd, profiles, canAll, canEdit, setRules, setProfiles, catalog, onDeleted }) {
  const T = window.CtorTypeMatrix;
  const [menuId, setMenuId] = React.useState(null);     // тип, у которого открыто меню «⋯»
  const [renameId, setRenameId] = React.useState(null); // тип, название которого правится на месте
  const [renameText, setRenameText] = React.useState('');
  const [editId, setEditId] = React.useState(null);     // тип, чей редактор «Правило и этапы» открыт
  const menuRef = React.useRef(null);
  const renameDone = React.useRef(false);
  const editable = !!(canEdit && setRules);
  // меню закрывается кликом мимо и по Esc
  React.useEffect(() => {
    if (!menuId) return undefined;
    const onDown = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuId(null); };
    const onKey = (e) => { if (e.key === 'Escape') setMenuId(null); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [menuId]);
  const startRename = (r) => { renameDone.current = false; setRenameText(r.name || ''); setRenameId(r.id); setMenuId(null); };
  // Enter / потеря фокуса — сохранить, Esc — отменить; пустое название не сохраняется; повторный вызов (blur после Enter) ничего не делает
  const finishRename = (save) => {
    if (renameDone.current) return;
    renameDone.current = true;
    const id = renameId;
    const name = renameText.trim();
    setRenameId(null);
    if (save && id && name) setRules((list) => list.map((x) => (x.id === id && x.name !== name ? { ...x, name } : x)));
  };
  const dropType = (r) => {
    setMenuId(null);
    if (!window.confirm('Удалить тип «' + (r.name || 'Без названия') + '»? Сметы с этим типом перейдут на общую библиотеку.')) return;
    ttDropType(r.id, setRules, setProfiles || (() => {}));
    if (onDeleted) onDeleted(r.id);
  };
  const editRule = editId ? (rules || []).find((x) => x.id === editId) : null;
  return (
    <div style={ttSt.bar}>
      <div className="bom-mode" role="tablist" aria-label="Тип объекта" style={ttSt.tabs}>
        {canAll && <button type="button" role="tab" aria-selected={!value} className={'bom-mode-btn' + (!value ? ' on' : '')} onClick={() => onPick('')}>Все работы</button>}
        {(rules || []).map((r) => {
          const n = T.exceptionCount(profiles, r.id) + T.settingsCount(profiles, r.id);
          const on = value === r.id;
          const name = r.name || 'Без названия';
          if (renameId === r.id) {
            return (
              <span key={r.id} style={ttSt.tabWrap} role="presentation">
                <input autoFocus style={ttSt.renameInput} value={renameText} aria-label={'Название типа объекта «' + name + '»'}
                  onChange={(e) => setRenameText(e.target.value)} onFocus={(e) => e.target.select()} onBlur={() => finishRename(true)}
                  onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); finishRename(true); } else if (e.key === 'Escape') { e.preventDefault(); finishRename(false); } }} />
              </span>);
          }
          return (
            <span key={r.id} ref={menuId === r.id ? menuRef : null} style={{ ...ttSt.tabWrap, background: on ? 'var(--card)' : 'transparent' }} role="presentation">
              <button type="button" role="tab" aria-selected={on} className={'bom-mode-btn' + (on ? ' on' : '')} onClick={() => onPick(r.id)}>
                {name}
                {n > 0 && <span style={ttSt.count} title={'Своих настроек вкладки (состав, цены, разметка): ' + n}>{n}</span>}
              </button>
              {editable && (
                <button type="button" style={ttSt.dots} aria-haspopup="menu" aria-expanded={menuId === r.id} title="Действия с типом объекта"
                  aria-label={'Действия с типом «' + name + '»'} onClick={() => setMenuId((cur) => (cur === r.id ? null : r.id))}>⋯</button>)}
              {editable && menuId === r.id && (
                <div style={ttSt.menu} role="menu" aria-label={'Тип «' + name + '»'}>
                  <button type="button" role="menuitem" style={ttSt.menuItem} onClick={() => startRename(r)}>Переименовать</button>
                  <button type="button" role="menuitem" style={ttSt.menuItem} onClick={() => { setMenuId(null); setEditId(r.id); }} title="Название, подбор по анкете, подпись для сметы, этапы дома и коммерции">Редактировать</button>
                  <button type="button" role="menuitem" disabled={!ttCanDelete(r.id)} style={{ ...ttSt.menuItem, ...ttSt.danger, ...(ttCanDelete(r.id) ? null : { opacity: 0.45, cursor: 'default' }) }}
                    title={ttCanDelete(r.id) ? undefined : 'Основной тип: на нём правится общий состав типов ремонта — удалить нельзя'} onClick={() => dropType(r)}>Удалить</button>
                </div>)}
            </span>
          );
        })}
      </div>
      {canEdit && <button type="button" style={ttSt.btn} onClick={onAdd}>+ тип объекта</button>}
      {editable && editRule && (
        <TaxObjectDrawer key={editRule.id} rule={editRule} rules={rules} catalog={catalog || []}
          onSave={(next) => setRules((list) => list.map((x) => (x.id === next.id ? next : x)))} onClose={() => setEditId(null)} />
      )}
    </div>
  );
}

/** вкладка типа помещения: шапка правила и матрица состава по типам ремонта */
function WorksTypeTab({ rule, rules, setRules, presets, setPresets, catalog, profiles, setProfiles, workKinds, checklists, canEdit, onOpenNode, presetFocus, onPresetFocusDone, onDeleted, autoIdx, onOpenPane, canAddRule }) {
  const T = window.CtorTypeMatrix;
  const [view, setView] = React.useState('compose'); // «Состав» (матрица) или «Цены и разметка» (профиль вкладки, очередь 3)
  const [q, setQ] = React.useState('');
  const [mode, setMode] = React.useState('all');
  const [wk, setWk] = React.useState('');
  const [editRule, setEditRule] = React.useState(false);
  const [presetOpen, setPresetOpen] = React.useState(null);
  const [presetMenu, setPresetMenu] = React.useState(null); // тип ремонта, у колонки которого открыто меню «⋯»
  const presetMenuRef = React.useRef(null);
  const [mergeOpenId, setMergeOpenId] = React.useState(''); // вкладка, у которой развёрнут список расхождений
  // меню колонки закрывается кликом мимо и по Esc
  React.useEffect(() => {
    if (!presetMenu) return undefined;
    const onDown = (e) => { if (presetMenuRef.current && !presetMenuRef.current.contains(e.target)) setPresetMenu(null); };
    const onKey = (e) => { if (e.key === 'Escape') setPresetMenu(null); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [presetMenu]);
  // поиск по Конструктору открыл тип ремонта — его свойства (только при праве правки: панель «просмотра» inert)
  React.useEffect(() => { if (presetFocus) { if (canEdit) setPresetOpen(presetFocus); if (onPresetFocusDone) onPresetFocusDone(); } }, [presetFocus]);
  const cols = React.useMemo(() => T.presetColumns(presets), [presets]);
  const base = rule.id === T.BASE_TYPE_ID;
  const all = React.useMemo(() => T.rows(catalog), [catalog]);
  // сетка ячеек — только при смене данных или фильтров (165 работ × типы ремонта)
  const grid = React.useMemo(() => T.filterRows(all, { profiles, rule, presets: cols, kinds: workKinds }, { q, mode, workKind: wk })
    .map((r) => ({ r, tasks: r.tasks.map((tk) => ({ tk, cells: cols.map((p) => ({ p, c: T.cell(profiles, rule, p, r.ph, r.st, tk) })) })) })),
  [all, profiles, rule, cols, workKinds, q, mode, wk]);
  const idx = (rules || []).findIndex((r) => r.id === rule.id);
  const exc = T.exceptionCount(profiles, rule.id);
  const shadow = rule.match && rule.match.propertyType ? taxObjectShadow(rules, rule) : null;
  const openPreset = presetOpen ? (presets || []).find((p) => p.id === presetOpen) : null;
  // условия типа объекта (этапы «от 2 этажей», «только офис») — только для чтения (задаёт сид, в редакторе их нет)
  const whenLines = React.useMemo(() => {
    const names = taxNodeNames(catalog);
    return (rule.onWhen || []).map((c) => taxWhenText(T.whenLabel(c), c, names));
  }, [rule.onWhen, catalog]);
  // отчёт о слиянии старых типов: у правила и у профиля вкладки; главнее второй тип (from[1])
  const profile = profiles && Object.prototype.hasOwnProperty.call(profiles, rule.id) ? profiles[rule.id] : null;
  const mergeLines = React.useMemo(() => [].concat(T.mergeLines(rule.merged, { catalog, presets, checklists }), T.mergeLines(profile && profile.merged, { catalog, presets, checklists })),
    [rule.merged, profile, catalog, presets, checklists]);
  const hasMerge = !!(rule.merged || (profile && profile.merged));
  const mergeFrom = [rule.merged, profile && profile.merged].map((m) => (m && Array.isArray(m.from) ? m.from : null)).find(Boolean) || ['', ''];
  const mergeOpen = mergeOpenId === rule.id;
  const dismissMerge = () => { setRules((list) => T.clearRuleMerged(list, rule.id)); setProfiles((pr) => T.clearProfileMerged(pr, rule.id)); };

  const move = (dir) => setRules((list) => {
    const i = list.findIndex((x) => x.id === rule.id), j = i + dir;
    if (i < 0 || j < 0 || j >= list.length) return list;
    const next = list.slice();
    next[i] = list[j]; next[j] = list[i];
    return next;
  });
  const resetAll = () => { if (confirm('Сбросить все исключения вкладки «' + (rule.name || 'Без названия') + '» (' + exc + ')? Останется состав типов ремонта и правила вкладки.')) setProfiles((p) => T.resetType(p, rule.id)); };
  const del = () => {
    if (!confirm('Удалить тип объекта «' + (rule.name || 'Без названия') + '»? Удалятся его исключения, цены и разметка: сметы этого типа вернутся к составу, ценам и срокам «Всех работ».')) return;
    ttDropType(rule.id, setRules, setProfiles);
    if (onDeleted) onDeleted();
  };
  const addPreset = () => { const p = T.newPreset(presets); setPresets((list) => [...list, p]); setPresetOpen(p.id); };
  const delPreset = (p) => {
    if (!confirm('Удалить тип ремонта «' + p.name + '»? Колонка уйдёт со всех вкладок вместе с исключениями; в собранных сметах ничего не изменится.')) return;
    setPresets((list) => list.filter((x) => x.id !== p.id));
    setProfiles((pr) => T.dropPreset(pr, p.id));
    setPresetOpen(null);
  };
  const click = (p, r, tk) => {
    if (!canEdit) return;
    if (base) { setPresets((list) => list.map((x) => (x.id === p.id ? T.toggleBase(x, rule, r.ph, r.st, tk) : x))); return; }
    setProfiles((pr) => T.toggleCell(pr, rule, p, r.ph, r.st, tk));
  };
  const stateText = (c) => (c.value ? (c.mark === '?' ? 'по условию анкеты' : 'входит') : 'не входит');
  const cellText = (c, p, tk) => {
    const lock = base && c.lock ? ' Не переключается: ' + TT_LOCK[c.lock] + '.' : '';
    const own = c.own ? ' Исключение вкладки; без него — «' + (c.base ? 'входит' : 'не входит') + '».' : '';
    const how = canEdit && !lock ? (base ? ' Клик — состав типа ремонта для всех вкладок.' : ' Клик — только для этой вкладки.') : '';
    return tk.name + ' · ' + p.name + ': ' + stateText(c) + '.' + own + lock + how;
  };

  return (
    <div>
      <div style={ttSt.head}>
        <div style={ttSt.headRow}>
          <div>
            <div style={ttSt.name}>{rule.name || 'Без названия'}</div>
            <div style={ttSt.muted}>Подбор по анкете: {taxObjectMatchLabel(rule.match)} · порядок {idx + 1} из {(rules || []).length}</div>
            {whenLines.map((l, i) => <div key={i} style={ttSt.muted}>{l}</div>)}
          </div>
          {canEdit && (
            <div style={ttSt.tools}>
              <button type="button" style={ttSt.btn} onClick={() => setEditRule(true)} title="Название, подбор по анкете, подпись для сметы, этапы дома и коммерции">Правило и этапы</button>
              {idx > 0 && <button type="button" style={ttSt.btn} onClick={() => move(-1)} title="Выше — в анкете проверяется раньше" aria-label="Выше">↑</button>}
              {idx < (rules || []).length - 1 && <button type="button" style={ttSt.btn} onClick={() => move(1)} title="Ниже" aria-label="Ниже">↓</button>}
              <button type="button" style={ttSt.btn} onClick={addPreset} title="Новая колонка на всех вкладках">+ тип ремонта</button>
              {exc > 0 && <button type="button" style={ttSt.btn} onClick={resetAll}>Сбросить исключения ({exc})</button>}
              {ttCanDelete(rule.id) && <button type="button" style={{ ...ttSt.btn, ...ttSt.danger }} onClick={del}>Удалить</button>}
            </div>
          )}
        </div>
        {rule.note && <div style={ttSt.note}>Подпись для сметы: {rule.note}</div>}
        {hasMerge && (
          <div style={ttSt.warn}>
            Объединены «{mergeFrom[0]}» и «{mergeFrom[1]}»: расхождений — {mergeLines.length}, главнее «{mergeFrom[1]}».
            {mergeLines.length > 0 && <button type="button" style={ttSt.mergeBtn} aria-expanded={mergeOpen} onClick={() => setMergeOpenId(mergeOpen ? '' : rule.id)}>{mergeOpen ? 'Скрыть' : 'Показать'}</button>}
            {canEdit && <button type="button" style={ttSt.mergeBtn} onClick={dismissMerge} title="Убрать отчёт">Понятно</button>}
            {mergeOpen && mergeLines.length > 0 && <ul style={ttSt.mergeList}>{mergeLines.map((l, i) => <li key={i}>{l}</li>)}</ul>}
          </div>)}
        {shadow && <div style={ttSt.warn}>⚠ Эти ответы анкеты уже закрывает «{shadow}» выше — поставьте точный тип выше общего.</div>}
        {base && <div style={ttSt.note}>«Квартира» — общий состав типов ремонта: клик по ячейке меняет тип ремонта для всех вкладок. Фазы и этапы целиком — в свойствах типа ремонта (заголовок колонки).</div>}
        {!canEdit && <div style={ttSt.warn}>Нет права правки шаблонов — вкладка только для просмотра.</div>}
      </div>

      <div className="bom-mode" role="tablist" aria-label="Что настраивается на вкладке" style={{ display: 'inline-flex', margin: '0 4px 12px' }}>
        {[{ v: 'compose', l: 'Состав' }, { v: 'settings', l: 'Цены и разметка' }].map((o) =>
          <button key={o.v} type="button" role="tab" aria-selected={view === o.v} className={'bom-mode-btn' + (view === o.v ? ' on' : '')} onClick={() => setView(o.v)}>{o.l}</button>)}
      </div>

      {view === 'settings' ? <WorksTypeSettings rule={rule} catalog={catalog} profiles={profiles} setProfiles={setProfiles} workKinds={workKinds} checklists={checklists} canEdit={canEdit} onOpenNode={onOpenNode} autoIdx={autoIdx} onOpenPane={onOpenPane} canAddRule={canAddRule} /> : (
      <React.Fragment>
      <div style={ttSt.filters}>
        <input type="search" style={ttSt.input} placeholder="Поиск работы" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Поиск работы" />
        <select style={ttSt.select} value={mode} onChange={(e) => setMode(e.target.value)} aria-label="Какие работы">
          <option value="all">Все работы</option>
          <option value="in">Только входящие</option>
          <option value="own">Только исключения вкладки</option>
        </select>
        <select style={ttSt.select} value={wk} onChange={(e) => setWk(e.target.value)} aria-label="Вид работ">
          <option value="">Все виды</option>
          {(workKinds || []).map((k) => <option key={k.id} value={k.id}>{k.label}</option>)}
          <option value="none">Без вида</option>
        </select>
        <span style={ttSt.muted}>● входит · — не входит · ? по условию анкеты · точка — исключение вкладки</span>
      </div>

      {!grid.length ? <div style={ttSt.empty}>Нет работ под этот фильтр</div> : (
        <div style={ttSt.scroll}>
          <table style={ttSt.table}>
            <colgroup>
              <col style={{ width: '34%' }} />
              {cols.map((p) => <col key={p.id} />)}
              <col style={{ width: 34 }} />
            </colgroup>
            <thead>
              <tr>
                <th style={{ ...ttSt.th, textAlign: 'left' }}>Работа</th>
                {cols.map((p) => (
                  <th key={p.id} ref={presetMenu === p.id ? presetMenuRef : null} style={{ ...ttSt.th, position: 'relative' }}>
                    {canEdit
                      ? <button type="button" style={ttSt.thBtn} onClick={() => setPresetOpen(p.id)} title="Свойства типа ремонта: состав по фазам и этапам, флаги, срок, видимость">{p.name}</button>
                      : p.name}
                    {canEdit && (
                      <button type="button" style={ttSt.dots} aria-haspopup="menu" aria-expanded={presetMenu === p.id} title="Действия с типом ремонта"
                        aria-label={'Действия с типом ремонта «' + p.name + '»'} onClick={() => setPresetMenu((cur) => (cur === p.id ? null : p.id))}>⋯</button>)}
                    {canEdit && presetMenu === p.id && (
                      <div style={{ ...ttSt.menu, left: 'auto', right: 0, minWidth: 150, fontWeight: 400 }} role="menu" aria-label={'Тип ремонта «' + p.name + '»'}>
                        <button type="button" role="menuitem" style={ttSt.menuItem} onClick={() => { setPresetMenu(null); setPresetOpen(p.id); }}>Свойства</button>
                        <button type="button" role="menuitem" disabled={T.isSeedPreset(p.id)}
                          style={{ ...ttSt.menuItem, ...ttSt.danger, ...(T.isSeedPreset(p.id) ? { opacity: 0.45, cursor: 'default' } : null) }}
                          title={T.isSeedPreset(p.id) ? 'Стандарт компании — не удаляется' : undefined}
                          onClick={() => { setPresetMenu(null); delPreset(p); }}>Удалить</button>
                      </div>)}
                  </th>
                ))}
                <th style={ttSt.th} aria-label="Сброс строки"></th>
              </tr>
            </thead>
            <tbody>
              {grid.map(({ r, tasks }) => (
                <React.Fragment key={r.ph.id + '/' + r.st.id}>
                  <tr><td style={ttSt.stage} colSpan={cols.length + 2}>{r.st.name}{' '}<WorksStageLabels info={autoIdx && autoIdx.stages[r.st.id]}
                    canAdd={!!canAddRule} onAdd={() => onOpenPane && onOpenPane('rules', 'intake', null)}
                    onIntake={(id) => onOpenPane && onOpenPane('rules', 'intake', id)}
                    onPackage={(id) => onOpenPane && onOpenPane('packages', null, id)}
                    onMore={(k) => onOpenPane && onOpenPane(k === 'packages' ? 'packages' : 'rules', k === 'packages' ? null : 'intake', null)} /></td></tr>
                  {tasks.map(({ tk, cells }) => (
                    <tr key={tk.id}>
                      <td style={ttSt.tdName}>
                        {onOpenNode
                          ? <button type="button" style={ttSt.workBtn} onClick={() => onOpenNode({ type: 'task', id: tk.id, phaseId: r.ph.id, stageId: r.st.id, taskId: tk.id })} title="Открыть работу во «Всех работах»">{tk.name}</button>
                          : tk.name}
                      </td>
                      {cells.map(({ p, c }) => {
                        const locked = base && !!c.lock;
                        const text = cellText(c, p, tk);
                        return (
                          <td key={p.id} style={ttSt.td}>
                            <button type="button" disabled={locked || !canEdit} onClick={() => click(p, r, tk)} title={text} aria-label={text}
                              style={{ ...ttSt.cell, ...(c.value ? null : ttSt.cellOff), ...(locked ? ttSt.cellLocked : null) }}>
                              {c.mark}{c.own && <span style={ttSt.dot} aria-hidden="true"></span>}
                            </button>
                          </td>
                        );
                      })}
                      <td style={ttSt.td}>
                        {canEdit && cells.some((x) => x.c.own) && <button type="button" style={ttSt.reset} onClick={() => setProfiles((p) => T.resetRow(p, rule.id, tk.id))} title="Сбросить исключения строки" aria-label={'Сбросить исключения: ' + tk.name}>↺</button>}
                      </td>
                    </tr>
                  ))}
                </React.Fragment>
              ))}
            </tbody>
          </table>
        </div>
      )}
      </React.Fragment>)}

      {canEdit && editRule && (
        <TaxObjectDrawer rule={rule} rules={rules} catalog={catalog}
          onSave={(next) => setRules((list) => list.map((x) => (x.id === next.id ? next : x)))} onClose={() => setEditRule(false)} />
      )}
      {canEdit && openPreset && (
        <PresetDrawer preset={openPreset} catalog={catalog}
          onChange={(patch) => setPresets((list) => list.map((x) => (x.id === openPreset.id ? { ...x, ...patch } : x)))} onClose={() => setPresetOpen(null)}
          onDelete={T.isSeedPreset(openPreset.id) ? null : () => delPreset(openPreset)} />
      )}
    </div>
  );
}

const ttFmt = (n) => (n ? Math.round(n).toLocaleString('ru-RU') + ' ₽' : '—');
const ttDays = (d) => (d ? String(d).replace('.', ',') + ' дн' : '—');
const ttHas = (o, k) => Object.prototype.hasOwnProperty.call(o || {}, k);
const ttOwnSt = { color: 'var(--accent-strong, #2563eb)', fontWeight: 600 };
const ttNumSt = { width: 92, height: 30, padding: '0 8px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 13, background: 'var(--card, #fff)', fontFamily: 'inherit' };
const ttSelSt = { height: 30, padding: '0 6px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 12.5, background: 'var(--card, #fff)', maxWidth: '100%' };

/** «Цены и разметка» вкладки типа помещения (спека §3, очередь 3): коэффициенты цены и срока, свои цена, срок и разметка
    работ. Пусто (серая подсказка) — значение из «Всех работ» с коэффициентом вкладки; своё — синим. «как во «Всех работах»» —
    значение библиотеки; «нет» у разметки — явно без неё на этой вкладке */
function WorksTypeSettings({ rule, catalog, profiles, setProfiles, workKinds, checklists, canEdit, onOpenNode, autoIdx, onOpenPane, canAddRule }) {
  const T = window.CtorTypeMatrix;
  const [q, setQ] = React.useState('');
  const [onlyOwn, setOnlyOwn] = React.useState(false);
  const [wk, setWk] = React.useState('');
  const all = React.useMemo(() => T.rows(catalog), [catalog]);
  const prof = (profiles && ttHas(profiles, rule.id) && profiles[rule.id]) || {};
  const grid = React.useMemo(() => T.filterRows(all, { profiles, rule, presets: [], kinds: workKinds }, { q, workKind: wk })
    .map((r) => ({ r, tasks: r.tasks
      .filter((tk) => !onlyOwn || Object.keys(T.ownValues(profiles, rule.id, tk.id)).length)
      .map((tk) => ({ tk, row: T.settingsRow(profiles, rule.id, r.ph, r.st, tk, catalog),
        lib: T.settingsRow(T.resetNodeValues(profiles, rule.id, tk.id), rule.id, r.ph, r.st, tk, catalog) })) }))
    .filter((x) => x.tasks.length), [all, profiles, rule, workKinds, q, wk, onlyOwn, catalog]);
  const n = T.settingsCount(profiles, rule.id);
  const set = (tk, key, value) => setProfiles((p) => T.setNodeValue(p, rule.id, tk.id, key, value));
  // ввод числа: пробелы (в т. ч. неразрывные), ₽ и «дн» убираем, запятая — десятичная. '' — снять своё; неверное — откат
  const parse = (s) => { const t = String(s).replace(/[\s\u00a0\u202f₽]|дн/g, '').replace(',', '.'); if (!t) return { empty: true }; const v = +t; return isFinite(v) && v > 0 ? { value: v } : { bad: true }; };
  // неверное или то же значение — поле показывает прежнее (в самом поле: пересоздание полей теряло бы фокус соседнего)
  const shown = (v) => (v == null ? '' : String(v).replace('.', ','));
  const commit = (tk, key, el, cur) => { const r = parse(el.value); if (r.bad) { el.value = shown(cur); return; } const v = r.empty ? undefined : key === 'days' ? Math.max(0.5, Math.round(r.value * 2) / 2) : r.value; if (v !== cur) set(tk, key, v); else el.value = shown(cur); };
  const clName = (id) => { const c = (checklists || []).find((x) => x.id === id); return c ? c.name : 'нет'; };
  const coef = (key, label, hint) => (
    <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }} title={hint}>
      {label} ×
      <input key={key + ':' + (prof[key] || 1)} type="text" inputMode="decimal" defaultValue={String(prof[key] || 1).replace('.', ',')} disabled={!canEdit}
        onBlur={(e) => { const r = parse(e.target.value); const v = r.empty ? 1 : r.value; if (r.bad || v < 0.1 || v > 10 || ((v < 0.5 || v > 3) && v !== (prof[key] || 1) && !confirm('Коэффициент ×' + String(v).replace('.', ',') + ' — точно? Обычно от 0,5 до 3.'))) { e.target.value = shown(prof[key] || 1); return; } setProfiles((p) => T.setCoef(p, rule.id, key, v)); }}
        style={{ ...ttNumSt, width: 64, ...(prof[key] ? ttOwnSt : null) }} aria-label={label + ': коэффициент вкладки'} />
    </label>
  );
  const resetAll = () => { if (confirm('Сбросить цены и разметку вкладки «' + (rule.name || 'Без названия') + '» (' + n + ')? Состав (исключения) останется.')) setProfiles((p) => T.resetSettings(p, rule.id)); };

  return (
    <div>
      <div style={ttSt.filters}>
        {coef('priceK', 'Цена', 'Коэффициент цены вкладки: себестоимость и цена/ед. работ × коэффициент, наценка та же; цена подрядчика не меняется')}
        {coef('daysK', 'Срок', 'Коэффициент срока вкладки: дни работ × коэффициент (шаг 0,5 дня)')}
        {canEdit && n > 0 && <button type="button" style={ttSt.btn} onClick={resetAll}>Сбросить цены и разметку ({n})</button>}
      </div>
      <div style={ttSt.filters}>
        <input type="search" style={ttSt.input} placeholder="Поиск работы" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Поиск работы" />
        <select style={ttSt.select} value={wk} onChange={(e) => setWk(e.target.value)} aria-label="Вид работ">
          <option value="">Все виды</option>
          {(workKinds || []).map((k) => <option key={k.id} value={k.id}>{k.label}</option>)}
          <option value="none">Без вида</option>
        </select>
        <label style={{ ...ttSt.muted, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
          <input type="checkbox" checked={onlyOwn} onChange={(e) => setOnlyOwn(e.target.checked)} /> только свои настройки
        </label>
        <span style={ttSt.muted}>серым — как во «Всех работах» с коэффициентом вкладки, синим — своё значение</span>
      </div>

      {!grid.length ? <div style={ttSt.empty}>Нет работ под этот фильтр</div> : (
        <div style={ttSt.scroll}>
          <table style={{ ...ttSt.table, minWidth: 980 }}>
            <colgroup>
              <col style={{ width: '28%' }} /><col style={{ width: '12%' }} /><col style={{ width: '9%' }} /><col style={{ width: '17%' }} />
              <col style={{ width: '11%' }} /><col style={{ width: '11%' }} /><col style={{ width: '9%' }} /><col style={{ width: 34 }} />
            </colgroup>
            <thead>
              <tr>
                <th style={{ ...ttSt.th, textAlign: 'left' }}>Работа</th><th style={ttSt.th}>Цена клиенту</th><th style={ttSt.th}>Срок</th>
                <th style={ttSt.th}>Чек-лист</th><th style={ttSt.th}>Фото при сдаче</th><th style={ttSt.th}>Согласование</th><th style={ttSt.th}>SMM</th>
                <th style={ttSt.th} aria-label="Сброс строки"></th>
              </tr>
            </thead>
            <tbody>
              {grid.map(({ r, tasks }) => (
                <React.Fragment key={r.ph.id + '/' + r.st.id}>
                  <tr><td style={ttSt.stage} colSpan={8}>{r.st.name}{' '}<WorksStageLabels info={autoIdx && autoIdx.stages[r.st.id]}
                    canAdd={!!canAddRule} onAdd={() => onOpenPane && onOpenPane('rules', 'intake', null)}
                    onIntake={(id) => onOpenPane && onOpenPane('rules', 'intake', id)}
                    onPackage={(id) => onOpenPane && onOpenPane('packages', null, id)}
                    onMore={(k) => onOpenPane && onOpenPane(k === 'packages' ? 'packages' : 'rules', k === 'packages' ? null : 'intake', null)} /></td></tr>
                  {tasks.map(({ tk, row, lib }) => {
                    const own = row.own;
                    const has = (k) => ttHas(own, k);
                    return (
                      <tr key={tk.id}>
                        <td style={ttSt.tdName}>
                          {onOpenNode
                            ? <button type="button" style={ttSt.workBtn} onClick={() => onOpenNode({ type: 'task', id: tk.id, phaseId: r.ph.id, stageId: r.st.id, taskId: tk.id })} title="Открыть работу во «Всех работах»">{tk.name}</button>
                            : tk.name}
                        </td>
                        <td style={ttSt.td}>
                          {row.bySub
                            ? <span style={ttSt.muted} title="Цена — по прайсу подрядчика (коэффициент вкладки к ней не применяется)">по прайсу</span>
                            : row.hasSubs
                            ? <span style={ttSt.muted} title="Цена работы — сумма подработ: свою цену задайте подработам во «Всех работах»">{ttFmt(row.price)}</span>
                            : <input key={'p:' + tk.id + ':' + (has('unitPrice') ? own.unitPrice : '')} type="text" inputMode="decimal" disabled={!canEdit}
                                defaultValue={has('unitPrice') ? own.unitPrice : ''} placeholder={ttFmt(lib.price)}
                                onBlur={(e) => commit(tk, 'unitPrice', e.target, own.unitPrice)}
                                style={{ ...ttNumSt, ...(has('unitPrice') ? ttOwnSt : null) }} aria-label={'Цена клиенту на вкладке: ' + tk.name} />}
                        </td>
                        <td style={ttSt.td}>
                          {row.hasSubs
                            ? <span style={ttSt.muted} title="Срок — по подработам">—</span>
                            : <input key={'d:' + tk.id + ':' + (has('days') ? own.days : '')} type="text" inputMode="decimal" disabled={!canEdit}
                                defaultValue={has('days') ? String(own.days).replace('.', ',') : ''} placeholder={ttDays(lib.days)}
                                onBlur={(e) => commit(tk, 'days', e.target, own.days)}
                                style={{ ...ttNumSt, width: 64, ...(has('days') ? ttOwnSt : null) }} aria-label={'Срок, дней, на вкладке: ' + tk.name} />}
                        </td>
                        <td style={ttSt.td}>
                          <select disabled={!canEdit} style={{ ...ttSelSt, ...(has('checklistId') ? ttOwnSt : null) }} aria-label={'Чек-лист на вкладке: ' + tk.name}
                            value={!has('checklistId') ? '' : own.checklistId === null ? '__none' : own.checklistId}
                            onChange={(e) => set(tk, 'checklistId', e.target.value === '' ? undefined : e.target.value === '__none' ? null : e.target.value)}>
                            <option value="">как во «Всех работах»: {lib.checklistId ? clName(lib.checklistId) : 'нет'}</option>
                            {(checklists || []).map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
                            <option value="__none">нет</option>
                          </select>
                        </td>
                        <td style={ttSt.td}>
                          <select disabled={!canEdit} style={{ ...ttSelSt, ...(has('photoOnClose') ? ttOwnSt : null) }} aria-label={'Фото при сдаче на вкладке: ' + tk.name}
                            value={!has('photoOnClose') || own.photoOnClose === null ? '' : own.photoOnClose ? 'yes' : 'no'}
                            onChange={(e) => set(tk, 'photoOnClose', e.target.value === '' ? undefined : e.target.value === 'yes')}>
                            <option value="">как во «Всех работах»: {lib.photo ? 'да' : 'нет'}</option>
                            <option value="yes">да</option>
                            <option value="no">нет</option>
                          </select>
                        </td>
                        <td style={ttSt.td}>
                          <select disabled={!canEdit} style={{ ...ttSelSt, ...(has('photoGate') ? ttOwnSt : null) }} aria-label={'Согласование на вкладке: ' + tk.name}
                            value={has('photoGate') && own.photoGate === null ? 'none' : ''}
                            onChange={(e) => set(tk, 'photoGate', e.target.value === 'none' ? null : undefined)}>
                            <option value="">как во «Всех работах»: {lib.gate ? 'есть' : 'нет'}</option>
                            <option value="none">нет</option>
                          </select>
                        </td>
                        <td style={ttSt.td}>
                          <select disabled={!canEdit} style={{ ...ttSelSt, ...(has('smm') ? ttOwnSt : null) }} aria-label={'SMM на вкладке: ' + tk.name}
                            value={has('smm') && own.smm === null ? 'none' : ''}
                            onChange={(e) => set(tk, 'smm', e.target.value === 'none' ? null : undefined)}>
                            <option value="">как во «Всех работах»: {lib.smm ? 'да' : 'нет'}</option>
                            <option value="none">нет</option>
                          </select>
                        </td>
                        <td style={ttSt.td}>
                          {canEdit && Object.keys(own).length > 0 && <button type="button" style={ttSt.reset} onClick={() => setProfiles((p) => T.resetNodeValues(p, rule.id, tk.id))} title="Сбросить к «Всем работам»" aria-label={'Сбросить настройки: ' + tk.name}>↺</button>}
                        </td>
                      </tr>
                    );
                  })}
                </React.Fragment>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { WorksTypeBar, WorksTypeTab, WorksTypeSettings });
