/* ctor-works-cells.jsx — «Работы», единая таблица (спека 2026-10-13-ctor-works-table §2): справочник «Вид работ», строка
   инструментов (наборы колонок, «Настроить», поиск, фильтры, счётчики), шапка колонок, ячейки «Разметки» и окно
   «Переместить в этап…». Колонки и фильтры — window.CtorWorks (ctor-works-model.js), правки разметки — window.CtorMarking
   (ctor-marking-model.js); строки и действия над каталогом — HierarchyPanel (ctor-hierarchy.jsx).
   У ctor-*.jsx общая область Babel — имена с префиксом Works / wk. */
const wkSt = {
  kinds: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap', margin: '0 0 10px' },
  kindChip: { display: 'inline-flex', alignItems: 'center', borderRadius: 999, background: 'var(--secondary)', fontSize: 12 },
  kindBtn: { background: 'none', border: 'none', padding: '3px 4px 3px 10px', cursor: 'pointer', font: 'inherit', color: 'var(--foreground)' },
  kindX: { background: 'none', border: 'none', padding: '3px 8px 3px 2px', cursor: 'pointer', font: 'inherit', color: 'var(--text-secondary)' },
  bar: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', margin: '0 0 4px' },
  input: { height: 32, padding: '0 10px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 13, minWidth: 200, background: 'var(--card, #fff)', fontFamily: 'inherit' },
  select: { height: 32, padding: '0 8px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 13, background: 'var(--card, #fff)', maxWidth: '100%', fontFamily: 'inherit', cursor: 'pointer' },
  cellSelect: { height: 28, padding: '0 6px', borderRadius: 8, border: '1px solid var(--border)', fontSize: 12.5, background: 'var(--card, #fff)', width: '100%', fontFamily: 'inherit' },
  counts: { fontSize: 12.5, color: 'var(--text-secondary)' },
  check: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer', whiteSpace: 'nowrap' },
  autoText: { fontSize: 12.5, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  link: { background: 'none', border: 'none', padding: '0 4px', cursor: 'pointer', fontSize: 12, color: 'var(--text-secondary)', textDecoration: 'underline', fontFamily: 'inherit' },
  muted: { fontSize: 12, color: '#8a817a' },
  // ячейка набора «Комнаты» — вид ячейки «Состава» вкладки типа (ctor-type-tabs.jsx)
  roomCell: { width: 34, height: 26, boxSizing: 'border-box', padding: 0, borderRadius: 7, border: '1px solid var(--border)', background: 'var(--card, #fff)', fontSize: 14, lineHeight: '24px', fontFamily: 'inherit', color: 'var(--foreground)' },
  roomOff: { color: 'var(--text-tertiary, #b4b2a9)' },
  pop: { position: 'absolute', top: 'calc(100% + 4px)', left: 0, zIndex: 50, background: 'var(--card, #fff)', border: '1px solid var(--border-subtle)', borderRadius: 10, padding: 6, boxShadow: 'var(--shadow-pop)', minWidth: 180 },
  popItem: { display: 'flex', alignItems: 'center', gap: 8, padding: '7px 8px', borderRadius: 8, fontSize: 13, cursor: 'pointer' },
  dialog: { position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(26,23,20,.28)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 },
  dialogBox: { background: 'var(--card, #fff)', borderRadius: 14, padding: 16, width: 'min(420px, 100%)', maxHeight: '80vh', overflowY: 'auto', boxShadow: 'var(--shadow-pop)' },
};
function wkPill(kind) {
  const tone = { on: ['#e6f3ea', '#2f6b45'], off: ['#f0ece5', '#8a817a'], gate: ['#fbf0dc', '#7a5410'], smm: ['#efeafd', '#4b3b8f'] }[kind] || ['#f0ece5', '#8a817a'];
  return { display: 'inline-block', padding: '2px 9px', borderRadius: 999, fontSize: 12, border: 'none', cursor: 'pointer', background: tone[0], color: tone[1], maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'left', fontFamily: 'inherit' };
}

/* справочник «Вид работ» (спека 2026-10-12-catalog-protection-base-check §5): клик по виду — переименовать, × — удалить;
   работы удалённого вида становятся «Без вида» (onRemoved снимает вид в каталоге) */
function WorksKindsBar({ kinds, setKinds, kindCounts, onRemoved }) {
  const list = kinds || [];
  const kc = kindCounts || {};
  const add = () => {
    const label = window.prompt('Новый вид работ');
    if (!label || !label.trim() || !setKinds) return;
    setKinds([...list, { id: 'wk_' + Date.now().toString(36), label: label.trim() }]);
  };
  const rename = (k) => {
    const label = window.prompt('Название вида работ', k.label);
    if (!label || !label.trim() || !setKinds) return;
    setKinds(list.map((x) => (x.id === k.id ? { ...x, label: label.trim() } : x)));
  };
  const remove = (k) => {
    if (!setKinds || !window.confirm('Удалить вид «' + k.label + '»? Его работы станут «Без вида».')) return;
    setKinds(list.filter((x) => x.id !== k.id));
    if (onRemoved) onRemoved(k.id);
  };
  return (
    <div style={wkSt.kinds}>
      <span style={wkSt.muted}>Виды работ:</span>
      {list.map((k) => (
        <span key={k.id} style={wkSt.kindChip}>
          <button type="button" style={wkSt.kindBtn} onClick={() => rename(k)} title="Переименовать">{k.label} · {kc[k.id] || 0}</button>
          {setKinds && <button type="button" style={wkSt.kindX} onClick={() => remove(k)} aria-label={'Удалить вид ' + k.label}>×</button>}
        </span>))}
      {kc.none ? <span style={wkSt.muted}>Без вида · {kc.none}</span> : null}
      {setKinds && <button type="button" style={wkSt.link} onClick={add}>+ вид</button>}
    </div>);
}

/* строка инструментов: набор колонок, «Настроить ▾» (доп. колонки), поиск, «Вид работ», «Показать», «Автоработы», счётчики разметки;
   autoOn / setAutoOn необязательные: нет setAutoOn — флажка нет. В наборе «Комнаты» (roomTypes — справочник, fold — свёрнутые группы,
   setFold) после наборов колонок — чипы групп: нажата — группа развёрнута по типам, отпущена — одна колонка; нет setFold — чипов нет */
function WorksToolbar({ cols, setCols, flt, setFlt, kinds, counts, onOpenChecklists, autoOn, setAutoOn, roomTypes, fold, setFold }) {
  const W = window.CtorWorks;
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return undefined;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);
  const set = (patch) => setFlt((x) => ({ ...x, ...patch }));
  const toggleExtra = (id) => setCols({ ...cols, extra: cols.extra.indexOf(id) !== -1 ? cols.extra.filter((x) => x !== id) : cols.extra.concat(id) });
  const c = counts || { photo: 0, noPhoto: 0, byChecklist: 0, smm: 0, gate: 0, noUnit: 0 };
  // «Доли» — колонки «Исполнителю / Прорабу / Компании» за ценой клиента; переключатель только у тех, кто видит доли (Calc2.shareCols), как в таблице
  const shareRole = W.shareRole(window.CtorRights ? window.CtorRights.me() : null);
  const canShares = W.shareKeysFor(shareRole).length > 0;
  const sharesNow = W.sharesShown(cols);
  const phone = useWorksPhone();
  const folded = W.readFold(fold);
  const groups = window.ConstructorData.ROOM_GROUPS.filter((g) => (roomTypes || []).some((t) => t.group === g.id));
  const toggleGroup = (id) => setFold(W.readFold(folded.indexOf(id) !== -1 ? folded.filter((x) => x !== id) : folded.concat(id)));
  return (
    <div style={wkSt.bar}>
      <div className="bom-mode" role="tablist" aria-label="Набор колонок" style={{ display: 'inline-flex' }}>
        {W.COLUMN_SETS.map((s) => (
          <button key={s.id} type="button" role="tab" aria-selected={cols.set === s.id} className={'bom-mode-btn' + (cols.set === s.id ? ' on' : '')}
            onClick={() => setCols({ ...cols, set: s.id })}>{s.label}</button>))}
      </div>
      {cols.set === 'rooms' && setFold && groups.length > 0 && (
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
          <span style={wkSt.muted}>Группы:</span>
          {groups.map((g) => {
            const open = folded.indexOf(g.id) === -1;
            return (
              <button key={g.id} type="button" className="chip chip--control" data-active={open ? 'true' : 'false'} aria-pressed={open}
                title={open ? 'Свернуть группу в одну колонку' : 'Развернуть группу'} onClick={() => toggleGroup(g.id)}>{g.label}</button>);
          })}
        </span>)}
      <div ref={ref} style={{ position: 'relative' }}>
        <button type="button" style={wkSt.select} aria-expanded={open} onClick={() => setOpen((o) => !o)}>Настроить ▾</button>
        {open && (
          <div style={wkSt.pop} role="group" aria-label="Доп. колонки">
            {canShares && (
              <label style={wkSt.popItem} title="Колонки «Исполнителю», «Прорабу», «Компании» за ценой клиента — по процентам из Экономика → Доли; в наборе «Основное»">
                <input type="checkbox" checked={sharesNow} disabled={phone} onChange={() => setCols({ ...cols, shares: !sharesNow })} />
                <span>Доли{phone ? ' (на телефоне не показываются)' : ''}</span>
              </label>)}
            {W.EXTRA_COLUMNS.map((col) => (
              <label key={col.id} style={wkSt.popItem}>
                <input type="checkbox" checked={cols.extra.indexOf(col.id) !== -1} onChange={() => toggleExtra(col.id)} />
                {col.label}
              </label>))}
          </div>)}
      </div>
      <input value={flt.q} onChange={(e) => set({ q: e.target.value })} placeholder="Поиск работы" style={wkSt.input} aria-label="Поиск работы" />
      <select value={flt.wk} onChange={(e) => set({ wk: e.target.value })} style={wkSt.select} aria-label="Вид работ">
        <option value="">Все виды</option>
        {(kinds || []).map((k) => <option key={k.id} value={k.id}>{k.label}</option>)}
        <option value="none">Без вида</option>
      </select>
      <select value={flt.show} onChange={(e) => set({ show: e.target.value })} style={wkSt.select} aria-label="Какие работы показать">
        {W.SHOW.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}
      </select>
      {setAutoOn && <label style={wkSt.check}><input type="checkbox" checked={autoOn !== false} onChange={(e) => setAutoOn(e.target.checked)} /> Автоработы</label>}
      <span style={wkSt.counts}>Фото: {c.photo} · без фото: {c.noPhoto} · по чек-листу: {c.byChecklist} · SMM: {c.smm} · согласование: {c.gate} · <span style={c.noUnit > 0 ? { color: 'var(--destructive)' } : undefined}>без ед.: {c.noUnit || 0}</span></span>
      {onOpenChecklists && <button type="button" style={wkSt.link} onClick={onOpenChecklists}>Шаблоны чек-листов →</button>}
    </div>);
}

/* ячейка набора «Комнаты»: ● отметка у всех узлов строки, ◐ у части, — ни у одного (вид ячейки «Состава» вкладки типа). С onClick — кнопка
   (ставит или снимает отметку), без него — просто знак (сводка по подработам, свёрнутая группа). title — и подсказка, и подпись для скринридера */
function WorksRoomCell({ state, title, onClick }) {
  const mark = state === 'all' ? '●' : state === 'some' ? '◐' : '—';
  const style = { ...wkSt.roomCell, ...(state === 'none' ? wkSt.roomOff : null) };
  if (onClick) {
    return <button type="button" onClick={onClick} aria-label={title} title={title} style={{ ...style, cursor: 'pointer' }}>{mark}</button>;
  }
  return <span role="img" aria-label={title} title={title} style={{ ...style, display: 'inline-block', textAlign: 'center' }}>{mark}</span>;
}

/* шапка: «Работа» и подписи колонок шириной в ячейку строки; справа — место под меню строки (30 px). labels — подписи комнатных колонок
   (CtorWorks.roomColumnLabels): у них подпись стоит вертикально (снизу вверх), а шапка прижата книзу, чтобы подписи были на одной линии */
function WorksHead({ colIds, labels }) {
  const W = window.CtorWorks;
  const names = labels || {};
  const label = (id) => (Object.prototype.hasOwnProperty.call(names, id) ? names[id] : (W.COLUMNS[id] ? W.COLUMNS[id].label : id));
  return (
    <div className={'works-head' + (colIds.some((id) => W.isRoomColumn(id)) ? ' works-head--rooms' : '')} role="row">
      <span style={{ flex: '1 1 auto', minWidth: 180 }}>Работа</span>
      {colIds.map((id) => (W.isRoomColumn(id)
        ? <span key={id} className="works-cell works-cell--room" style={{ width: W.colWidth(id) }}><span className="works-vlabel" title={label(id)}>{label(id)}</span></span>
        : <span key={id} className="works-cell" style={{ width: W.colWidth(id) }}>{label(id)}</span>))}
      <span style={{ width: 30, flexShrink: 0 }}></span>
    </div>);
}

/* ячейки строки: render[id]() — содержимое колонки id; нет — пустая ячейка той же ширины */
function WorksCells({ colIds, render }) {
  const W = window.CtorWorks;
  return (
    <React.Fragment>
      {colIds.map((id) => <span key={id} className={'works-cell' + (W.isRoomColumn(id) ? ' works-cell--room' : '')} style={{ width: W.colWidth(id) }}>{render[id] ? render[id]() : null}</span>)}
    </React.Fragment>);
}

/* сумма в ячейке цены: с пробелами между разрядами и «₽»; ноль и пусто — «—» (общий вид для цены работы и строки автоработы) */
function wkMoney(v) { return v ? v.toLocaleString('ru-RU') + ' ₽' : '—'; }

/* телефон (CtorWorks.SHARE_PHONE_QUERY, ≤ 900 px): колонки долей на нём не рисуются — таблица и без них шире экрана, лишние 340 px расширили бы страницу */
function useWorksPhone() {
  const q = window.CtorWorks.SHARE_PHONE_QUERY;
  const get = () => (typeof window.matchMedia === 'function' ? window.matchMedia(q).matches : false);
  const [on, setOn] = React.useState(get);
  React.useEffect(() => {
    if (typeof window.matchMedia !== 'function') return undefined;
    const mq = window.matchMedia(q);
    const h = () => setOn(mq.matches);
    h();
    if (mq.addEventListener) mq.addEventListener('change', h); else mq.addListener(h);
    return () => { if (mq.removeEventListener) mq.removeEventListener('change', h); else mq.removeListener(h); };
  }, []);
  return on;
}

/* доля цены клиента в ячейке «Исполнителю / Прорабу / Компании»: сумма и процент от цены клиента; s — CtorWorks.sharesOfNode, k — executor | foreman | company,
   tablePrice — цена в колонке «Цена клиенту» (подсказка называет обе цены, если они расходятся). Нет цены у работы — «—» */
function WorksShareCell({ s, k, tablePrice }) {
  const W = window.CtorWorks;
  if (!s) return <span className="works-share works-share--none" style={{ color: 'var(--text-tertiary, #b4b2a9)' }}>—</span>;
  return (
    <span className="works-share" title={W.shareTip(s, k, tablePrice)}>
      <span className="works-share-sum">{wkMoney(s[k])}</span>
      <span className="works-share-pct">{W.sharePctText(s, k)}</span>
    </span>);
}

/* цена клиенту = себестоимость + наценка; в ячейке не правится — клик открывает настройки работы на стоимости */
function WorksPriceCell({ value, onClick }) {
  return (
    <button type="button" className="chip chip--money" onClick={onClick} style={{ cursor: 'pointer' }}
      title="Цена клиенту — себестоимость и наценка. Открыть в настройках работы">{wkMoney(value)}</button>);
}

/* единица измерения листовой работы (ConstructorData.UNITS; спека 2026-10-18-estimate-rooms-design §1): value — id единицы или пусто,
   onChange(id | null) — пустой выбор «—» снимает единицу, label — для скринридера. Без единицы рамка цвета destructive: каталог с такой работой не опубликовать */
function WorksUnitCell({ value, onChange, label }) {
  const CD = window.ConstructorData;
  const cur = CD.UNITS.some((u) => u.id === value) ? value : '';
  return (
    <select value={cur} onChange={(e) => onChange(e.target.value || null)} aria-label={label}
      style={cur ? wkSt.cellSelect : { ...wkSt.cellSelect, borderColor: 'var(--destructive)' }}
      title={cur ? undefined : 'Нет единицы — каталог не опубликовать'}>
      <option value="">—</option>
      {CD.UNITS.map((u) => <option key={u.id} value={u.id}>{u.label}</option>)}
    </select>);
}

/* норма выработки листовой работы — сколько единиц делает одно звено за смену (8 ч); спека 2026-10-19-work-norms §1. Нормы нет у работы
   комплектом и у работы во времени — вместо поля «—»; пока единицы нет, «—» подсказывает выбрать её. Поле с черновиком (NumField deferred): «0,5» набирается по
   символам, значение уходит на blur или Enter. onChange(число | null): пустое поле — null (нормы нет), явный 0 — 0 (владелец снял норму; сид её не возвращает) */
function WorksNormCell({ node, onChange }) {
  const CD = window.ConstructorData;
  if (!CD.normApplies(node)) {
    return <span style={{ color: 'var(--text-secondary)' }} title={CD.unitOf(node) ? 'Нормы нет: единица времени или комплект' : 'Сначала выберите единицу'}>—</span>;
  }
  return (
    <NumField deferred value={node.norm == null ? '' : node.norm} onChange={onChange}
      suffix={CD.unitLabel(node.unit) + '/см'} min={0} step={0.5} title={'Норма выработки: ' + CD.normLabel(node.unit)} />);
}

/* ---- ячейки «Разметки» (бывшая «Таблица», MkgPanel): r — строка CtorMarking.rows, edit(fn) — правка каталога ---- */
function WorksKindCell({ r, kinds, edit }) {
  const M = window.CtorMarking;
  const label = (id) => { const k = (kinds || []).find((x) => x.id === id); return k ? k.label : 'без вида'; };
  return (
    <select value={r.kindOwn && r.kind ? r.kind : ''} onChange={(e) => edit((cat) => M.setWorkKind(cat, r.sel, e.target.value))}
      style={wkSt.cellSelect} aria-label={'Вид работ: ' + r.name}>
      <option value="">{label(r.kindDefault) + ' · по умолч.'}</option>
      {(kinds || []).map((k) => <option key={k.id} value={k.id}>{k.label}</option>)}
    </select>);
}
function WorksChecklistCell({ r, checklists, edit }) {
  const M = window.CtorMarking;
  const CD = window.ConstructorData;
  const labelOf = (c) => (CD && CD.checklistOptionLabel ? CD.checklistOptionLabel(c) : c.name);
  return (
    <select value={r.checklistId || ''} onChange={(e) => edit((cat) => M.setChecklist(cat, r.sel, e.target.value))}
      style={wkSt.cellSelect} aria-label={'Чек-лист: ' + r.name}>
      <option value="">—</option>
      {(checklists || []).map((c) => <option key={c.id} value={c.id}>{labelOf(c)}</option>)}
    </select>);
}
function WorksPhotoCell({ r, edit }) {
  const M = window.CtorMarking;
  if (r.photo === 'checklist') return <span style={wkSt.muted} title="Открепите чек-лист, чтобы включить фото по умолчанию">по чек-листу</span>;
  return (
    <button type="button" style={wkPill(r.photo === 'on' ? 'on' : 'off')} title={r.photoByRule ? 'По умолчанию — нажмите, чтобы выбрать явно' : ''}
      onClick={() => edit((cat) => M.setPhoto(cat, r.sel, r.photo !== 'on'))}>{r.photo === 'on' ? 'вкл' : 'выкл'}{r.photoByRule ? ' · по умолч.' : ''}</button>);
}
function WorksGateCell({ r, edit }) {
  const M = window.CtorMarking;
  return (
    <button type="button" style={wkPill(r.gateOn ? 'gate' : 'off')} title={r.gateLabel || 'Включить: согласует прораб, блокирует следующие этапы'}
      onClick={() => edit((cat) => M.setGate(cat, r.sel, !r.gateOn))}>{r.gateOn ? r.gateLabel : 'нет'}</button>);
}
function WorksSmmCell({ r, edit }) {
  const M = window.CtorMarking;
  return (
    <button type="button" style={wkPill(r.smmOn ? 'smm' : 'off')} title={r.smmLabel || 'Включить: формат «Результат»'}
      onClick={() => edit((cat) => M.setSmm(cat, r.sel, !r.smmOn))}>{r.smmOn ? r.smmLabel : 'нет'}</button>);
}

/* строка этапа в «Разметке»: вкл / выкл у всех работ этапа (работы с чек-листом массовое «Фото» не трогает) */
function WorksStageMass({ title, onSet }) {
  return (
    <span title={title} style={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}>
      <span style={wkSt.muted}>этап:</span>
      <button type="button" style={wkSt.link} onClick={() => onSet(true)}>вкл</button>
      <button type="button" style={wkSt.link} onClick={() => onSet(false)}>выкл</button>
    </span>);
}

/* «Переместить в этап…»: этапы по фазам; текущий этап отмечен и не выбирается; Esc или клик мимо — закрыть */
function WorksMovePicker({ catalog, taskName, fromStageId, onPick, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);
  return (
    <div role="dialog" aria-modal="true" aria-label={'Переместить «' + taskName + '» в этап'} style={wkSt.dialog} onClick={onClose}>
      <div style={wkSt.dialogBox} onClick={(e) => e.stopPropagation()}>
        <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>Переместить «{taskName}» в этап</div>
        {(catalog || []).map((ph) => (
          <div key={ph.id} style={{ marginBottom: 8 }}>
            <div style={wkSt.muted}>{ph.name}</div>
            {(ph.stages || []).map((st) => {
              const here = st.id === fromStageId;
              return (
                <button key={st.id} type="button" disabled={here} onClick={() => onPick(st.id)}
                  style={{ ...ctorStyles.dropdownItem, opacity: here ? 0.45 : 1, cursor: here ? 'default' : 'pointer' }}>
                  <span style={{ width: 8, height: 8, borderRadius: '50%', background: st.color || '#8a817a', flexShrink: 0 }}></span>
                  {st.name}{here ? ' · сейчас здесь' : ''}
                </button>);
            })}
          </div>))}
        <button type="button" onClick={onClose} style={ctorStyles.btnGhost}>Отмена</button>
      </div>
    </div>);
}

/* склонение числительного: 1 пункт, 2 пункта, 5 пунктов, 11 пунктов, 21 пункт */
function wkPlural(n, one, few, many) {
  const a = Math.abs(n) % 100;
  const b = a % 10;
  if (a > 10 && a < 20) return many;
  if (b === 1) return one;
  if (b > 1 && b < 5) return few;
  return many;
}

/* число из поля правила (число или строка); не число — null */
function wkNum(v) {
  if (v === null || v === undefined || v === '') return null;
  const n = typeof v === 'number' ? v : parseFloat(v);
  return Number.isFinite(n) ? n : null;
}

/* вкладки раздела «Работы»: «Работы» / «Пакеты услуг» / «Автоправила»; вкладка видна, только если can[id] (права раздела) */
const wkPanes = [
  { id: 'works', label: 'Работы' },
  { id: 'packages', label: 'Пакеты услуг' },
  { id: 'rules', label: 'Автоправила' },
];
function WorksPaneBar({ value, onPick, can }) {
  const allow = can || {};
  return (
    <div className="bom-mode" role="tablist" aria-label="Вкладки раздела «Работы»" style={{ display: 'inline-flex', flexWrap: 'wrap', maxWidth: '100%' }}>
      {wkPanes.filter((p) => allow[p.id]).map((p) => (
        <button key={p.id} type="button" role="tab" aria-selected={value === p.id} className={'bom-mode-btn' + (value === p.id ? ' on' : '')}
          onClick={() => onPick(p.id)}>{p.label}</button>))}
    </div>);
}

/* вид автоправил на вкладке «Автоправила»: заготовки по анкете (intake) / перекрёстные работы (cross) */
const wkRuleKinds = [
  { id: 'intake', label: 'Заготовки по анкете' },
  { id: 'cross', label: 'Перекрёстные работы' },
];
function WorksRulesKindSeg({ value, onPick }) {
  return (
    <div className="bom-mode" role="tablist" aria-label="Вид автоправил" style={{ display: 'inline-flex', flexWrap: 'wrap', maxWidth: '100%' }}>
      {wkRuleKinds.map((k) => (
        <button key={k.id} type="button" role="tab" aria-selected={value === k.id} className={'bom-mode-btn' + (value === k.id ? ' on' : '')}
          onClick={() => onPick(k.id)}>{k.label}</button>))}
    </div>);
}

/* метки в строке этапа: «Автоподгрузка при выборе в анкете: …» (работы подставятся в смету сами, когда в анкете выбран пункт) и
   «Пакет услуг: …» — первые три, остальные скрыты за «+N» (onMore открывает вкладку правил или пакетов); у автоподгрузки ещё «+ правило»
   (onAdd — вкладка «Автоправила»; canAdd — у человека есть эта вкладка, иначе кнопки нет);
   info — запись этапа из CtorWorks.autoIndex: intake = [{ id, label }], packages = [{ id, name }]; нет ни заготовок, ни пакетов — ничего */
function WorksStageLabels({ info, onIntake, onPackage, onMore, onAdd, canAdd }) {
  const intake = (info && info.intake) || [];
  const pkgs = (info && info.packages) || [];
  if (!intake.length && !pkgs.length) return null;
  const MAX = 3;
  // клик по метке не должен схлопывать или выделять строку этапа, в которой она стоит
  const stop = (fn) => (e) => { e.stopPropagation(); if (fn) fn(); };
  const group = (cls, prefix, list, nameOf, onItem, moreKey, tail) => (
    <span className={'works-label ' + cls}>
      {prefix}
      {list.slice(0, MAX).map((x, i) => (
        <React.Fragment key={i + ':' + x.id}>
          {i > 0 ? ', ' : null}
          <button type="button" onClick={stop(() => onItem && onItem(x.id))}>{nameOf(x)}</button>
        </React.Fragment>))}
      {list.length > MAX && <React.Fragment>{' '}<button type="button" onClick={stop(() => onMore && onMore(moreKey))}>+{list.length - MAX}</button></React.Fragment>}
      {tail}
    </span>);
  const addRule = canAdd && onAdd ? <React.Fragment>{' '}<button type="button" className="works-label-add" onClick={stop(() => onAdd())}>+ правило</button></React.Fragment> : null;
  return (
    <React.Fragment>
      {intake.length > 0 && group('works-label--intake', 'Автоподгрузка при выборе в анкете: ', intake, (x) => x.label, onIntake, 'intake', addRule)}
      {intake.length > 0 && pkgs.length > 0 ? ' ' : null}
      {pkgs.length > 0 && group('works-label--pkg', 'Пакет услуг: ', pkgs, (x) => x.name, onPackage, 'packages')}
    </React.Fragment>);
}

/* «+N пункт / пункта / пунктов по правилам» — пункты чек-листа, которые добавят перекрёстные правила; n = 0 — ничего */
function WorksChecksNote({ n }) {
  if (!n) return null;
  return <span className="works-label">+{n} {wkPlural(n, 'пункт', 'пункта', 'пунктов')} по правилам</span>;
}

/* строка работы, которую добавит смежное (перекрёстное) правило (CtorWorks.autoIndex → Item): не перетаскивается, клик / Enter / Space открывает
   правило. Слева название и под ним серым «Смежная задача: направление → этап «имя» · правило «имя»» (place — «этап «имя»» или «конец
   фазы»; направление — название роли, как в ячейке «Исполнитель»), условие правила (whenText) — в подсказке строки. Ячейки: исполнитель,
   срок, цена («—», если цены нет); справа место 30 px вместо меню. onRole(roleId) есть у тех, кто правит правила: тогда «Исполнитель» —
   выпадающий выбор роли (тот же RolePill, что у обычной работы, но без «Без роли» и только из roleChoices — направления перекрёстных
   правил, как в редакторе правила); клик по нему строку не открывает; нет onRole — просто текст.
   Фон и скругление — у блока WorksAutoBlock, сама строка как строка работы */
function WorksAutoRow({ item, colIds, roles, roleChoices, whenText, place, onOpen, onRole }) {
  const open = () => { if (onOpen) onOpen(); };
  const role = (roles || []).find((r) => r.id === item.role);
  const days = wkNum(item.days);
  const price = wkNum(item.price);
  // варианты выбора: направления правил; роль работы вне их остаётся в списке, чтобы выбор показывал её, а не «роль»
  const choices = roleChoices || roles || [];
  const pickRoles = role && !choices.some((r) => r.id === role.id) ? choices.concat(role) : choices;
  // выбор роли не должен открывать правило: ни клик, ни Enter / Space внутри выбора до строки не доходят
  const stopRow = (e) => e.stopPropagation();
  const render = {
    executor: () => (onRole
      ? <span style={{ display: 'inline-flex', minWidth: 0 }} onClick={stopRow} onKeyDown={stopRow}><RolePill roleId={item.role} roles={pickRoles} allowEmpty={false} onChange={(v) => onRole(v)} /></span>
      : (role ? <span style={wkSt.autoText} title={role.name}>{role.name}</span> : null)),
    days: () => (days === null ? null : <span style={wkSt.autoText}>{String(days).replace('.', ',')} дн</span>),
    price: () => (price === null ? null : <span style={wkSt.autoText}>{wkMoney(price)}</span>),
  };
  // направление — как в правиле («Сантехника», «Общестрой»); нет в справочнике — имя роли
  const dir = ((window.ConstructorData && window.ConstructorData.CROSS_DIRECTIONS) || []).find((d) => d.id === item.role);
  const dirName = dir ? dir.label : (role ? role.name : '');
  const note = 'Смежная задача' + (dirName ? ': ' + dirName : '') + (place ? ' → ' + place : '') + (item.ruleName ? ' · правило «' + item.ruleName + '»' : '');
  return (
    <div className="works-row works-auto" role="button" tabIndex={0} onClick={open} title={whenText || undefined}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }}>
      <span className="works-name">
        <span className="works-auto-title">{item.name}</span>
        <span className="works-auto-when">{note}</span>
      </span>
      <WorksCells colIds={colIds} render={render} />
      <span style={{ width: 30, flexShrink: 0 }}></span>
    </div>);
}

/* автоработы одного места (начало этапа / сразу под работой / конец этапа / «В конце фазы») — один блок с лёгким фоном:
   заголовок «Смежные работы · N» и строки; place — куда они встанут («этап «имя»» / «конец фазы»), whenByRule — тексты условий по id
   правила (подсказка строки), onOpen(ruleId) открывает правило, onRole(ruleId, idx, roleId) меняет роль работы в правиле (нет — выбора роли нет),
   roleChoices — роли, из которых её можно выбрать (направления перекрёстных правил) */
function WorksAutoBlock({ items, place, colIds, roles, roleChoices, whenByRule, onOpen, onRole }) {
  if (!items || !items.length) return null;
  const when = whenByRule || {};
  return (
    <div className="works-auto-block" role="group" aria-label="Смежные работы">
      <div className="works-auto-head">Смежные работы · {items.length}</div>
      {items.map((item) => (
        <WorksAutoRow key={item.ruleId + ':' + item.idx} item={item} colIds={colIds} roles={roles} roleChoices={roleChoices} place={place}
          whenText={Object.prototype.hasOwnProperty.call(when, item.ruleId) ? when[item.ruleId] : ''}
          onOpen={() => { if (onOpen) onOpen(item.ruleId); }}
          onRole={onRole ? (v) => onRole(item.ruleId, item.idx, v) : null} />))}
    </div>);
}

Object.assign(window, { WorksKindsBar, WorksToolbar, WorksRoomCell, WorksHead, WorksCells, WorksPriceCell, WorksShareCell, useWorksPhone, WorksUnitCell, WorksNormCell, WorksKindCell, WorksChecklistCell, WorksPhotoCell, WorksGateCell, WorksSmmCell, WorksStageMass, WorksMovePicker, WorksPaneBar, WorksRulesKindSeg, WorksStageLabels, WorksChecksNote, WorksAutoRow, WorksAutoBlock, wkPlural });
