/* ctor-inspector.jsx — правая панель свойств узла (stage / task / work) */
const { useState: inUseState } = React;

/* ---- мелкие поля ---- */
const insStyles = {
  panel: {
    width: 364, flexShrink: 0, alignSelf: 'flex-start', position: 'sticky', top: 20,
    background: 'var(--glass-bg-strong)', WebkitBackdropFilter: 'var(--glass-blur)', backdropFilter: 'var(--glass-blur)',
    borderRadius: 'var(--radius-2xl)', border: '1px solid var(--glass-border)',
    boxShadow: 'var(--shadow-card)',
    maxHeight: 'calc(100vh - 40px)', overflowY: 'auto'
  },
  head: { padding: '18px 20px 14px', borderBottom: '1px solid var(--border-subtle)', position: 'sticky', top: 0, background: 'var(--glass-bg-strong)', WebkitBackdropFilter: 'var(--glass-blur)', backdropFilter: 'var(--glass-blur)', zIndex: 2, borderRadius: 'var(--radius-2xl) var(--radius-2xl) 0 0' },
  body: { padding: '16px 20px 28px', display: 'flex', flexDirection: 'column', gap: 20 },
  sec: { display: 'flex', flexDirection: 'column', gap: 7 },
  secLabel: { fontSize: 11, fontWeight: 700, color: 'var(--c-subtle)', textTransform: 'uppercase', letterSpacing: '.06em' },
  row: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 },
  label: { fontSize: 11.5, color: 'var(--text-secondary)', fontWeight: 600 },
  hint: { fontSize: 11, color: 'var(--c-subtle)', marginTop: 1 },
  field: { fontFamily: 'inherit', fontSize: 12.5, color: 'var(--foreground)', background: 'var(--input-background)', border: '1px solid var(--c-border)', borderRadius: 8, padding: '5px 9px', width: '100%', outline: 'none' },
  numSm: { width: 92, textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontWeight: 600 },
  mono: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 }
};

function FText({ label, value, onChange, hint, placeholder, mono, multiline }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      <span style={insStyles.label}>{label}</span>
      {multiline ?
      <textarea value={value || ''} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} rows={2}
      style={{ ...insStyles.field, resize: 'vertical', lineHeight: 1.45 }} /> :
      <input value={value || ''} onChange={(e) => onChange(e.target.value)} placeholder={placeholder}
      style={{ ...insStyles.field, ...(mono ? insStyles.mono : {}) }} />}
      {hint && <span style={insStyles.hint}>{hint}</span>}
    </label>);

}
function FNum({ label, value, onChange, suffix, step = 1, min = 0, hint }) {
  return (
    <div style={insStyles.row}>
      <div><div style={insStyles.label}>{label}</div>{hint && <div style={insStyles.hint}>{hint}</div>}</div>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#f0ece5', border: '.5px solid #e4ddd2', borderRadius: 9, padding: '5px 10px' }}>
        <input type="number" value={value == null ? '' : value} step={step} min={min}
        onChange={(e) => onChange(e.target.value === '' ? null : Math.max(min, +e.target.value || 0))}
        style={{ ...insStyles.field, ...insStyles.numSm, background: 'transparent', border: 'none', padding: 0, width: 56, flex: 1, minWidth: 0 }} />
        {suffix && <span style={{ fontSize: 11.5, color: '#8a817a', fontWeight: 500, whiteSpace: 'nowrap' }}>{suffix}</span>}
      </span>
    </div>);

}
function FToggle({ label, value, onChange, hint }) {
  return (
    <div style={insStyles.row}>
      <div><div style={insStyles.label}>{label}</div>{hint && <div style={insStyles.hint}>{hint}</div>}</div>
      <div onClick={() => onChange(!value)} style={{ width: 32, height: 19, borderRadius: 11, background: value ? '#5aad6e' : '#e4ddd2', position: 'relative', transition: 'background .2s', flexShrink: 0, cursor: 'pointer' }}>
        <div style={{ width: 15, height: 15, borderRadius: '50%', background: '#fff', position: 'absolute', top: 2, transform: value ? 'translateX(15px)' : 'translateX(2px)', transition: 'transform .2s cubic-bezier(.32,.72,0,1)', boxShadow: '0 1px 3px rgba(0,0,0,.18)' }}></div>
      </div>
    </div>);

}
const INC_SEG_OPTS = [
  { v: 'required', l: 'обязательно' },
  { v: 'optional', l: 'опционально' },
  { v: 'conditional', l: 'по условию' },
];
function effectiveInc(node) {
  return node.inc || (node.cond ? 'conditional' : 'required');
}
function InclusionFields({ node, patch }) {
  const inc = effectiveInc(node);
  return (
    <FSeg
      label="Включение в смету"
      value={inc}
      onChange={(v) => patch({ inc: v })}
      options={INC_SEG_OPTS}
      hint="обязательно · опционально · по условию"
    />
  );
}

function DependsOnPicker({ catalog, selfId, value, onAdd, excludeIds }) {
  const [q, setQ] = inUseState('');
  const CD = window.ConstructorData;
  const ex = new Set(excludeIds || []);
  const ql = (q || '').trim().toLowerCase();
  const rows = (CD.flattenCatalog ? CD.flattenCatalog(catalog) : []).filter((row) => {
    if (row.type === 'phase') return false;
    if (row.node.id === selfId || ex.has(row.node.id)) return false;
    if (CD.isDescendantOf && CD.isDescendantOf(catalog, selfId, row.node.id)) return false;
    const label = (row.node.name || '') + ' ' + (CD.buildNodePath ? CD.buildNodePath(catalog, row.node.id) : '');
    return !ql || label.toLowerCase().includes(ql);
  });
  return (
    <div className="dep-picker">
      <div className="dep-picker-search">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#a89e92" strokeWidth="2"><circle cx="11" cy="11" r="7" /><line x1="16.5" y1="16.5" x2="21" y2="21" /></svg>
        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск этапа, задачи, подзадачи…" />
      </div>
      <div className="dep-picker-tree">
        {rows.length === 0 && <div style={{ padding: 10, fontSize: 12, color: '#a89e92' }}>Ничего не найдено</div>}
        {rows.slice(0, 40).map((row) => {
          const path = CD.buildNodePath ? CD.buildNodePath(catalog, row.node.id) : row.node.name;
          const typeLbl = row.type === 'stage' ? 'этап' : row.type === 'work' ? 'подзадача' : 'задача';
          return (
            <button key={row.node.id} type="button" className="dep-picker-row" onClick={() => onAdd({ nodeId: row.node.id, nodeType: row.type, path })}>
              <span className="dep-picker-type">{typeLbl}</span>
              <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{path}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

function ConditionSection({ node, patch, catalog, selfId }) {
  const inc = effectiveInc(node);
  if (inc !== 'conditional') return null;
  const deps = node.dependsOn || [];
  const selectedIds = new Set(deps.map((d) => d.nodeId));
  const addDep = (d) => {
    if (selectedIds.has(d.nodeId)) return;
    patch({ dependsOn: [...deps, d], dependsMode: node.dependsMode || 'all', inc: 'conditional' });
  };
  const removeDep = (nodeId) => patch({ dependsOn: deps.filter((x) => x.nodeId !== nodeId) });
  return (
    <section style={insStyles.sec}>
      <div style={insStyles.secLabel}>Условие</div>
      <div style={insStyles.hint}>Узел попадает в смету, когда все выбранные зависимости включены.</div>
      {deps.length > 0 && (
        <div className="dep-list">
          {deps.map((d) => (
            <div key={d.nodeId} className="dep-list-item">
              <span className="dep-list-item-path" title={d.path}>{d.path || d.nodeId}</span>
              <button type="button" onClick={() => removeDep(d.nodeId)} title="Убрать зависимость">{Icon.x}</button>
            </div>
          ))}
        </div>
      )}
      <DependsOnPicker catalog={catalog} selfId={selfId} value={deps} onAdd={addDep} excludeIds={[...selectedIds]} />
      {node.cond && !deps.length && (
        <div style={{ ...insStyles.hint, color: '#b91c1c', marginTop: 4 }}>
          Устаревшее условие (cond): <code style={{ fontFamily: 'ui-monospace, monospace' }}>{node.cond}</code> — выберите узлы каталога выше.
        </div>
      )}
      {node.cond && deps.length > 0 && (
        <FText label="Legacy cond (устар.)" value={node.cond} onChange={(v) => patch({ cond: v })} mono hint="Сохранено для совместимости; в смете используется dependsOn." />
      )}
    </section>
  );
}
function FSeg({ label, value, onChange, options, hint }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      <div style={insStyles.label}>{label}</div>
      <div style={{ display: 'flex', gap: 2, padding: 2, background: '#f0ece5', borderRadius: 8 }}>
        {options.map((o) =>
        <button key={String(o.v)} onClick={() => onChange(o.v)} style={{
          flex: 1, padding: '6px 8px', borderRadius: 8, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
          fontSize: 12.5, fontWeight: 600, transition: 'all .15s', whiteSpace: 'nowrap',
          background: value === o.v ? '#fff' : 'transparent',
          color: value === o.v ? o.color || '#1a1714' : '#8a817a',
          boxShadow: value === o.v ? '0 1px 2px rgba(0,0,0,.08)' : 'none'
        }}>{o.l}</button>
        )}
      </div>
      {hint && <span style={insStyles.hint}>{hint}</span>}
    </div>);

}
function FSelect({ label, value, onChange, options, placeholder }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      <span style={insStyles.label}>{label}</span>
      <select value={value || ''} onChange={(e) => onChange(e.target.value || null)} style={{ ...insStyles.field, appearance: 'auto', cursor: 'pointer' }}>
        <option value="">{placeholder || '—'}</option>
        {options.map((o) => <option key={o.v} value={o.v}>{o.l}</option>)}
      </select>
    </label>);

}

/* визуальные флаги видимости (по ролям платформы) */
const VIS_ROLES = [
{ k: 'client', l: 'Клиент' }, { k: 'manager', l: 'Менеджер' }, { k: 'foreman', l: 'Прораб' },
{ k: 'executor', l: 'Исполнитель' }, { k: 'designer', l: 'Дизайнер' }];

function VisibilityFlags({ value, onChange }) {
  const v = value || { client: true, manager: true, foreman: true, executor: true, designer: true };
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
      {VIS_ROLES.map((r) => {
        const on = v[r.k] !== false;
        return (
          <button key={r.k} onClick={() => onChange({ ...v, [r.k]: !on })} style={{
            padding: '3px 9px', borderRadius: 980, border: '.5px solid ' + (on ? '#e8793a40' : '#e4ddd2'),
            background: on ? '#fff0e6' : '#f0ece5', color: on ? '#e8793a' : '#a89e92',
            fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit'
          }}>{r.l}</button>);

      })}
    </div>);

}

const NODE_TYPE_LABEL = { stage: 'Этап', task: 'Задача', work: 'Подзадача', phase: 'Фаза' };

/** Горизонтальная навигация по разделам инспектора (как сегменты в дереве сметы) */
function InspectorSectionNav({ sections, active, onChange, ariaLabel }) {
  const visible = (sections || []).filter((s) => !s.hidden);
  if (!visible.length) return null;
  return (
    <div className="ins-nav" role="tablist" aria-label={ariaLabel || 'Разделы настроек'}>
      {visible.map((s) => (
        <button
          key={s.id}
          type="button"
          role="tab"
          aria-selected={active === s.id}
          className={'ins-nav-item' + (active === s.id ? ' on' : '')}
          onClick={() => onChange(s.id)}
        >
          <span>{s.label}</span>
          {s.badge != null && s.badge !== 0 && (
            <span className="ins-nav-badge">{s.badge}</span>
          )}
        </button>
      ))}
    </div>
  );
}

/** Нижняя зона «Вложения и доступ» — как в InspectorInline дерева сметы */
function InspectorFooterBand({ title, children }) {
  return (
    <div className="ins-footer-band">
      {title && <div className="ins-footer-title">{title}</div>}
      <div className="ins-footer-grid">{children}</div>
    </div>
  );
}

function InspectorFooterBlock({ title, children, className }) {
  return (
    <div className={'ins-footer-block' + (className ? ' ' + className : '')}>
      {title && <div className="ins-footer-block-title">{title}</div>}
      {children}
    </div>
  );
}

function InspectorEmptyState({ title, hint }) {
  return (
    <div className="ins-empty-tab">
      <div style={{ fontSize: 14.5, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: hint ? 6 : 0 }}>{title}</div>
      {hint && <div style={{ fontSize: 13, lineHeight: 1.5, color: 'var(--text-tertiary)' }}>{hint}</div>}
    </div>
  );
}

/** Оболочка inline-инспектора: head → nav → body → footer */
function InspectorShell({ head, nav, footer, children, compact, className }) {
  return (
    <div className={'ins-shell ctor-fade' + (compact ? ' ins-shell--compact' : '') + (className ? ' ' + className : '')}>
      {head}
      {nav}
      <div className="ins-body">{children}</div>
      {footer}
    </div>
  );
}

function InspectorHead({ badges, title, subtitle, onClose, compact, typeLabel, typeColor, roleAbbr, roleColor }) {
  if (compact) {
    return (
      <div className="ins-head ins-head--compact">
        {typeLabel && (
          <span style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.05em', color: '#fff', background: typeColor || '#8a817a', padding: '2px 8px', borderRadius: 980, flexShrink: 0 }}>
            {typeLabel}
          </span>
        )}
        {roleAbbr && (
          <span style={{ width: 17, height: 17, borderRadius: '50%', background: roleColor, color: '#fff', fontSize: 7.5, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{roleAbbr}</span>
        )}
        <div className="ins-head-title">{title}</div>
        {onClose && <button type="button" onClick={onClose} style={{ ...ctorStyles.iconBtnSm, color: 'var(--text-tertiary)', width: 20, height: 20 }} title="Закрыть">{Icon.x}</button>}
      </div>
    );
  }
  return (
    <div className="ins-head">
      <div className="ins-head-row">
        <div className="ins-head-badges">{badges}</div>
        {onClose && <button type="button" onClick={onClose} style={{ ...ctorStyles.iconBtnSm, color: 'var(--text-tertiary)' }} title="Закрыть">{Icon.x}</button>}
      </div>
      {title && <h2 className="ins-head-title">{title}</h2>}
      {subtitle && <div className="ins-head-sub">{subtitle}</div>}
    </div>
  );
}

function InspectorSection({ title, lead, children, plain, full }) {
  return (
    <section className={'ins-section' + (plain ? ' ins-section--plain' : '') + (full ? ' ins-section--full' : '')}>
      {title && <h3 className="ins-section-title">{title}</h3>}
      {lead && <p className="ins-section-lead">{lead}</p>}
      <div className="ins-section-fields">{children}</div>
    </section>
  );
}

function InspectorFieldGrid({ cols, children }) {
  const mod = cols === 3 ? ' ins-grid--3' : cols === 1 ? ' ins-grid--1' : ' ins-grid--2';
  return <div className={'ins-grid' + mod}>{children}</div>;
}

function InspectorCallout({ variant, children }) {
  const v = variant === 'success' ? ' ins-callout--success' : variant === 'warn' ? ' ins-callout--warn' : ' ins-callout--info';
  return <div className={'ins-callout' + v}>{children}</div>;
}

function InspectorMetric({ label, value, accent }) {
  return (
    <div className={'ins-metric' + (accent ? ' ins-metric--accent' : '')}>
      <span className="ins-metric-label">{label}</span>
      <span className="ins-metric-value">{value}</span>
    </div>
  );
}

/** Slide-over drawer для редакторов настроек (типы полей, пресеты, опции…) */
function InspectorDrawer({ title, badges, subtitle, onClose, footer, children, width = 560 }) {
  return (
    <div className="ins-drawer-root" role="dialog" aria-modal="true" aria-labelledby="ins-drawer-title">
      <button type="button" className="ins-drawer-backdrop" onClick={onClose} aria-label="Закрыть панель" />
      <div className="ins-drawer-panel ctor-fade" style={{ '--ins-drawer-w': width + 'px' }}>
        <InspectorHead
          badges={badges}
          title={title}
          subtitle={subtitle}
          onClose={onClose}
        />
        <div className="ins-drawer-body">
          <div className="ins-tabpanel ins-tabpanel--drawer">{children}</div>
        </div>
        {footer ? <div className="ins-drawer-foot">{footer}</div> : null}
      </div>
    </div>
  );
}

function Inspector({ selected, node, patch, roles, onClose, notes, setNotes, onTabSwitch, catalog }) {
  const cat = catalog || (window.ConstructorData && window.ConstructorData.loadCatalog ? window.ConstructorData.loadCatalog() : []);
  if (!selected || !node) {
    return (
      <aside style={insStyles.panel}>
        <div style={{ padding: '54px 28px', textAlign: 'center', color: '#a89e92' }}>
          <div style={{ width: 48, height: 48, borderRadius: 14, background: '#f0ece5', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 14px', color: '#bcb3a7' }}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
          </div>
          <div style={{ fontSize: 14.5, fontWeight: 600, color: '#8a817a' }}>Инспектор узла</div>
          <div style={{ fontSize: 13, marginTop: 6, lineHeight: 1.5 }}>Выберите этап, задачу или подзадачу в дереве — здесь появятся все свойства, сроки и правила включения.</div>
        </div>
      </aside>);

  }
  const t = selected.type;
  const role = roles.find((r) => r.id === node.role);

  return (
    <aside style={insStyles.panel} className="ctor-fade" key={node.id}>
      <div style={insStyles.head}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.06em', color: '#fff', background: t === 'stage' ? node.color || '#8a817a' : t === 'task' ? '#e8793a' : '#8a817a', padding: '3px 9px', borderRadius: 980 }}>
              {NODE_TYPE_LABEL[t]}
            </span>
            {role && <span style={{ width: 20, height: 20, borderRadius: '50%', background: role.color, color: '#fff', fontSize: 9.5, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{role.abbr}</span>}
          </span>
          <button onClick={onClose} style={{ ...ctorStyles.iconBtnSm, color: '#bcb3a7' }} title="Закрыть">{Icon.x}</button>
        </div>
        <div style={{ fontSize: 17, fontWeight: 600, letterSpacing: '-.015em', color: '#1a1714', marginTop: 12, lineHeight: 1.25 }}>{node.name}</div>
        <div style={{ ...insStyles.mono, color: '#a89e92', marginTop: 4 }}>{node.slug || node.id}</div>
      </div>

      <div style={insStyles.body}>
        {/* ОСНОВНОЕ */}
        <section style={insStyles.sec}>
          <div style={insStyles.secLabel}>Основное</div>
          <FText label="Название" value={node.name} onChange={(v) => patch({ name: v })} />
          {/* Slug скрыт из инспектора (§10.1) — остаётся в данных node.slug */}
          <FText label="Описание" value={node.description} onChange={(v) => patch({ description: v })} placeholder="Короткое пояснение узла" multiline />
          <div style={{ display: 'flex', gap: 10 }}>
            <div style={{ flex: 1 }}><FNum label="Порядок" value={node.sortOrder == null ? 0 : node.sortOrder} onChange={(v) => patch({ sortOrder: v })} /></div>
          </div>
          <FToggle label="Активен" value={node.isActive !== false} onChange={(v) => patch({ isActive: v })} hint="Неактивные узлы не попадают в смету" />
        </section>

        {/* STAGE-СПЕЦИФИКА */}
        {t === 'stage' &&
        <section style={insStyles.sec}>
            <div style={insStyles.secLabel}>Этап</div>
            <FText label="Подсказка / note" value={node.note} onChange={(v) => patch({ note: v })} placeholder="Серый хинт под названием" multiline />
          </section>
        }

        {(t === 'stage' || t === 'task' || t === 'work') &&
        <section style={insStyles.sec}>
          <div style={insStyles.secLabel}>Включение</div>
          <InclusionFields node={node} patch={patch} />
        </section>
        }

        {(t === 'stage' || t === 'task' || t === 'work') &&
        <ConditionSection node={node} patch={patch} catalog={cat} selfId={node.id} />
        }

        {/* СРОКИ — stage/task/work */}
        <section style={insStyles.sec}>
          <div style={insStyles.secLabel}>Сроки и нормирование</div>
          {t === 'task' && <FNum label="Фикс. дни" value={node.days} onChange={(v) => patch({ days: v })} suffix="раб.дн" step={0.5} />}
          <FNum label="Норма, дн/м²" value={node.workdaysPerSqm} onChange={(v) => patch({ workdaysPerSqm: v })} suffix="дн/м²" step={0.01} hint="Если задаётся по площади" />
          <FNum label="Технический простой" value={t === 'task' ? node.pause : node.techPauseDays} onChange={(v) => patch(t === 'task' ? { pause: v } : { techPauseDays: v })} suffix="дн" hint="Сушка / выдержка после узла" />
          <FNum label="Зимний коэфф." value={node.winterMultiplier} onChange={(v) => patch({ winterMultiplier: v })} suffix="×" step={0.05} />
          <FNum label="Размер бригады" value={node.defaultCrewSize} onChange={(v) => patch({ defaultCrewSize: v })} suffix="чел" />
          <FToggle label="Работа в выходные" value={!!node.allowWeekends} onChange={(v) => patch({ allowWeekends: v })} />
          <FToggle label="Только мокрые зоны" value={!!node.wetZonesOnly} onChange={(v) => patch({ wetZonesOnly: v })} />
        </section>

        {t === 'task' &&
        <section style={insStyles.sec}>
            <div style={insStyles.secLabel}>Задача</div>
            <FSelect label="Роль" value={node.role} onChange={(v) => patch({ role: v })} options={roles.map((r) => ({ v: r.id, l: r.name }))} placeholder="Без роли" />
            <FSeg label="Требуемый уровень" value={node.level || ''} onChange={(v) => patch({ level: v })}
          options={[{ v: '', l: 'любой' }, { v: 'elite', l: 'Элита 0–1', color: '#7c3aed' }, { v: 'pro', l: 'Профи 2–3', color: '#e8793a' }]} />
          </section>
        }

        {t === 'work' &&
        <section style={insStyles.sec}>
            <div style={insStyles.secLabel}>Подзадача (work)</div>
            <FSelect label="Роль" value={node.role} onChange={(v) => patch({ role: v })} options={roles.map((r) => ({ v: r.id, l: r.name }))} placeholder="Без роли" />
            <div style={{ display: 'flex', gap: 10 }}>
              <div style={{ flex: 1 }}><FNum label="Часы / ед." value={node.hoursPerUnit} onChange={(v) => patch({ hoursPerUnit: v })} suffix="ч" step={0.5} /></div>
            </div>
            <FSeg label="Единица" value={node.unit || 'm2'} onChange={(v) => patch({ unit: v })}
          options={[{ v: 'm2', l: 'м²' }, { v: 'lm', l: 'м' }, { v: 'pcs', l: 'шт' }]} />
            <FNum label="Цена за единицу" value={node.unitPrice} onChange={(v) => patch({ unitPrice: v })} suffix="₽" step={50} />
            <FText label="Связь с work-types" value={node.workTypeSlug} onChange={(v) => patch({ workTypeSlug: v })} placeholder="work-type slug" mono />
          </section>
        }

        {/* СТОИМОСТЬ РАБОТ — task/work редактируемо; stage = сумма детей */}
        {(() => {
          const nodeClient = (n) => {
            const c = +n.cost || 0,m = +n.margin || 0;
            return c + Math.round(c * m / 100);
          };
          const sumChildren = (n) => {
            if (n.tasks) return n.tasks.reduce((a, tk) => a + (tk.sub ? tk.sub.reduce((b, w) => b + nodeClient(w), 0) || nodeClient(tk) : nodeClient(tk)), 0);
            if (n.sub) return n.sub.reduce((a, w) => a + nodeClient(w), 0);
            return 0;
          };
          if (t === 'stage') {
            const sum = sumChildren(node);
            return (
              <section style={insStyles.sec}>
                <div style={insStyles.secLabel}>Стоимость этапа</div>
                <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '12px 14px', background: '#f0ece5', borderRadius: 11 }}>
                  <span style={{ fontSize: 12.5, color: '#8a817a' }}>Сумма задач и подзадач</span>
                  <span style={{ fontSize: 18, fontWeight: 800, color: '#1a1714', fontVariantNumeric: 'tabular-nums' }}>{sum.toLocaleString('ru-RU')} ₽</span>
                </div>
                <div style={insStyles.hint}>Стоимость этапа формируется автоматически из входящих задач и подзадач.</div>
              </section>);

          }
          const c = +node.cost || 0,m = +node.margin || 0,tax = +node.tax || 0;
          const marginRub = Math.round(c * m / 100);
          const client = c + marginRub;
          const withTax = Math.round(client * (1 + tax / 100));
          return (
            <section style={insStyles.sec}>
              <div style={insStyles.secLabel}>Стоимость работ</div>
              <FNum label="Себестоимость" value={node.cost} onChange={(v) => patch({ cost: v })} suffix="₽" step={100} />
              <FNum label="Индексация себестоимости" value={node.costIndex == null ? 5 : node.costIndex} onChange={(v) => patch({ costIndex: v })} suffix="%/мес" step={0.5} hint="Автоматический рост себестоимости в месяц" />
              <div style={{ position: 'relative' }}>
                <FNum label="Маржа" value={node.margin == null ? 30 : node.margin} onChange={(v) => patch({ margin: v })} suffix="%" step={1} />
                <span style={{ position: 'absolute', right: 14, top: 30, fontSize: 11.5, fontWeight: 700, color: '#2f7d52', pointerEvents: 'none' }}>(+{marginRub.toLocaleString('ru-RU')} ₽)</span>
              </div>
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '11px 14px', background: '#f0ece5', borderRadius: 11, marginTop: 4 }}>
                <span style={{ fontSize: 12.5, fontWeight: 600, color: '#8a817a' }}>Цена для клиента</span>
                <span style={{ fontSize: 17, fontWeight: 800, color: '#1a1714', fontVariantNumeric: 'tabular-nums' }}>{client.toLocaleString('ru-RU')} ₽</span>
              </div>
              <FNum label="Налог" value={node.tax == null ? 0 : node.tax} onChange={(v) => patch({ tax: v })} suffix="%" step={1} />
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '9px 14px', borderRadius: 11, border: '.5px solid #ece5da', marginTop: 4 }}>
                <span style={{ fontSize: 12, color: '#a89e92' }}>Стоимость с налогом</span>
                <span style={{ fontSize: 14, fontWeight: 700, color: '#5c5249', fontVariantNumeric: 'tabular-nums' }}>{withTax.toLocaleString('ru-RU')} ₽</span>
              </div>
            </section>);

        })()}

        {/* ЗАМЕТКИ ПРОЕКТА — краткий счётчик + переход */}
        {notes && (() => {
          const cnt = (notes[node.id] || []).length;
          return (
            <section style={insStyles.sec}>
              <div style={insStyles.secLabel}>Заметки</div>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: '#f0ece5', borderRadius: 11, border: '.5px solid #ece5da' }}>
                <span style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13.5, fontWeight: 500, color: '#1a1714' }}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={cnt > 0 ? '#e8793a' : '#bcb3a7'} strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
                  {cnt > 0 ?
                  <span>{cnt} {cnt === 1 ? 'заметка' : cnt < 5 ? 'заметки' : 'заметок'}</span> :
                  <span style={{ color: '#a89e92' }}>Нет заметок</span>}
                </span>
                {onTabSwitch &&
                <button onClick={() => onTabSwitch('notes')} style={{ fontSize: 12.5, fontWeight: 600, color: '#e8793a', background: 'none', border: 'none', cursor: 'pointer', padding: '4px 10px', borderRadius: 7, fontFamily: 'inherit' }}>
                    {cnt > 0 ? 'Открыть →' : 'Добавить →'}
                  </button>
                }
              </div>
            </section>);

        })()}

        {/* ЧЕК-ЛИСТ ATTACH */}
        <ChecklistAttachSection nodeId={node.id} node={node} patch={patch} checklists={window._ctorChecklists || []} />

        {/* АНКЕТА ЗАМЕРА ATTACH — на этап / задачу / подзадачу */}
        <MeasureAttachSection nodeId={node.id} node={node} patch={patch} measures={window._ctorMeasures || []} nodeType={t} />

        {/* ОПЦИИ CHIPS */}
        <OptionsChipsSection nodeId={node.id} node={node} patch={patch} options={window._ctorOptions || []} />

        {/* ВИДИМОСТЬ */}
        <section style={insStyles.sec}>
          <div style={insStyles.secLabel}>Видимость по ролям</div>
          <VisibilityFlags value={node.visibility} onChange={(v) => patch({ visibility: v })} />
          <div style={insStyles.hint}>Где этот узел показывается в смете и интерфейсах ролей.</div>
        </section>
      </div>
    </aside>);

}

/* ---- Анкета замера attach (целиком на задачу / этап) ---- */
function MeasureAttachSection({ nodeId, node, patch, measures, nodeType }) {
  const attached = node.measureId || null;
  const ms = measures.find((m) => m.id === attached);
  const scopeWord = nodeType === 'stage' ? 'этапу' : nodeType === 'work' ? 'подзадаче' : 'задаче';
  const scopeWordGen = nodeType === 'stage' ? 'этапа' : nodeType === 'work' ? 'подзадачи' : 'задачи';
  return (
    <section style={insStyles.sec}>
      <div style={insStyles.secLabel}>Анкета замера</div>
      {attached && ms ?
      <div style={{ background: '#f0ece5', borderRadius: 10, padding: '8px 11px', display: 'flex', flexDirection: 'column', gap: 5 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 15 }}>📐</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, color: '#1a1714' }}>{ms.name}</div>
              <div style={{ fontSize: 11, color: '#8a817a' }}>{(ms.fields || []).length} полей · на весь {scopeWord}</div>
            </div>
            <button onClick={() => patch({ measureId: null })} style={{ ...ctorStyles.iconBtnSm, color: '#bcb3a7' }} title="Открепить">{Icon.x}</button>
          </div>
          <FToggle label={'Обязательна перед стартом ' + scopeWordGen} value={!!node.measureRequired} onChange={(v) => patch({ measureRequired: v })} />
          <FToggle label="Применить ко всем дочерним узлам" value={node.measureInherit !== false} onChange={(v) => patch({ measureInherit: v })} hint={'Анкета распространяется на весь ' + scopeWord + ' целиком'} />
          <div style={{ borderTop: '.5px solid #ece5da', margin: '2px 0', paddingTop: 7 }}>
            <FSeg label="Как приходит замерщику" value={node.measureDelivery || 'embedded'} onChange={(v) => patch({ measureDelivery: v })}
          options={[{ v: 'embedded', l: 'Форма в задаче' }, { v: 'link', l: 'Прямая ссылка' }]} />
            <div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, marginTop: 6, fontSize: 11, color: '#8a817a', lineHeight: 1.45 }}>
              <span style={{ flexShrink: 0 }}>{(node.measureDelivery || 'embedded') === 'link' ? '🔗' : '🧾'}</span>
              <span>{(node.measureDelivery || 'embedded') === 'link' ?
              'Задача на замеры доступна по прямой ссылке — без входа в ЛК (удобно для стороннего замерщика).' :
              'Задача на замеры приходит таском со встроенной формой в ЛК исполнителя.'}</span>
            </div>
          </div>
        </div> :

      <div>
          <FSelect label={'Прикрепить анкету ко всему ' + scopeWord} value={''} onChange={(v) => v && patch({ measureId: v, measureInherit: true })}
        options={measures.map((m) => ({ v: m.id, l: m.name }))} placeholder="— выбрать шаблон замера —" />
          {measures.length === 0 ?
        <span style={insStyles.hint}>Создайте шаблон во вкладке «Шабл. замеров»</span> :
        <span style={insStyles.hint}>Анкета (форма замера) настраивается сразу на весь {scopeWord}</span>}
        </div>
      }
    </section>);

}

/* ---- Чек-лист attach ---- */
function ChecklistAttachSection({ nodeId, node, patch, checklists }) {
  const attached = node.checklistId || null;
  const cl = checklists.find((c) => c.id === attached);
  return (
    <section style={insStyles.sec}>
      <div style={insStyles.secLabel}>Чек-лист</div>
      {attached && cl ?
      <div style={{ background: '#f0ece5', borderRadius: 10, padding: '8px 11px', display: 'flex', flexDirection: 'column', gap: 5 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 15 }}>📋</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, color: '#1a1714' }}>{cl.name}</div>
              <div style={{ fontSize: 11, color: '#8a817a' }}>{cl.items.length} пунктов · {cl.items.filter((i) => i.required).length} обяз.</div>
            </div>
            <button onClick={() => patch({ checklistId: null })} style={{ ...ctorStyles.iconBtnSm, color: '#bcb3a7' }} title="Открепить">{Icon.x}</button>
          </div>
          <FToggle label="Обязателен для закрытия задачи" value={!!node.checklistRequired} onChange={(v) => patch({ checklistRequired: v })} />
          <FToggle label="Наследовать дочерним узлам" value={!!node.checklistInherit} onChange={(v) => patch({ checklistInherit: v })} />
        </div> :

      <div>
          <FSelect label="Прикрепить шаблон" value={''} onChange={(v) => v && patch({ checklistId: v })}
        options={checklists.map((c) => ({ v: c.id, l: c.name }))} placeholder="— выбрать шаблон —" />
          {checklists.length === 0 && <span style={insStyles.hint}>Создайте шаблон во вкладке «Чек-листы»</span>}
        </div>
      }
    </section>);

}

/* ---- Опции chips (read-only link) ---- */
function OptionsChipsSection({ nodeId, node, patch, options }) {
  const linked = node.linkedOptions || [];
  const linkedOpts = options.filter((o) => linked.includes(o.id));
  const unlinked = options.filter((o) => !linked.includes(o.id));
  const toggle = (id) => {
    const next = linked.includes(id) ? linked.filter((x) => x !== id) : [...linked, id];
    patch({ linkedOptions: next });
  };
  return (
    <section style={insStyles.sec}>
      <div style={insStyles.secLabel}>Опции</div>
      {options.length === 0 ?
      <div style={insStyles.hint}>Создайте опции во вкладке «Опции»</div> :

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {options.map((o) => {
          const on = linked.includes(o.id);
          return (
            <button key={o.id} onClick={() => toggle(o.id)} style={{
              display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 980, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, transition: 'all .15s',
              background: on ? o.color + '18' : '#f0ece5',
              color: on ? o.color : '#8a817a',
              border: on ? `1px solid ${o.color}44` : '1px solid transparent'
            }}>{o.icon} {o.name}</button>);

        })}
        </div>
      }
      {linkedOpts.length > 0 &&
      <div style={{ fontSize: 12, color: '#8a817a', marginTop: 2 }}>Привязано к {linkedOpts.map((o) => o.name).join(', ')}</div>
      }
    </section>);

}

/* ---- Готовые шаблоны заметок (выбор как у опций) ---- */
const NOTE_TEMPLATES = [
{ id: 'nt-damper', title: 'Демпферная лента', text: 'Демпферная лента по периметру обязательна. Маяки снять после схватывания.' },
{ id: 'nt-press', title: 'Пресс-тест труб', text: 'Опрессовка системы 6 атм, выдержка 30 мин, зафиксировать давление до/после.' },
{ id: 'nt-wp2', title: 'Гидроизоляция 2 слоя', text: 'Гидроизоляция обмазочная в 2 слоя, заход на стены 200 мм, усиление лентой по углам.' },
{ id: 'nt-hidden', title: 'Фото скрытых работ', text: 'Сделать фотоотчёт скрытых работ до закрытия (трубы, проводка, армирование).' },
{ id: 'nt-level', title: 'Контроль уровня', text: 'Проверить уровень/плоскость правилом 2 м, отклонение не более 2 мм.' },
{ id: 'nt-clean', title: 'Уборка после этапа', text: 'Вынести мусор, обеспылить поверхности перед следующим этапом.' }];


/* ---- Заметки как теги (toggle активна/выкл) + попап выбора/создания, по образцу Опций ---- */
const { useState: ncUseState, useRef: ncUseRef, useEffect: ncUseEffect } = React;

function NoteChipsSection({ nodeId, node, notes, setNotes }) {
  const list = notes && notes[nodeId] || [];
  const [open, setOpen] = ncUseState(false);
  const [tab, setTab] = ncUseState('lib'); // lib | new
  const [draft, setDraft] = ncUseState('');
  const [q, setQ] = ncUseState('');
  const wrapRef = ncUseRef(null);

  ncUseEffect(() => {
    if (!open) return;
    const onDown = (e) => {if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);};
    const onKey = (e) => {if (e.key === 'Escape') setOpen(false);};
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {document.removeEventListener('mousedown', onDown);document.removeEventListener('keydown', onKey);};
  }, [open]);

  const toggle = (noteId) => setNotes((all) => ({ ...all, [nodeId]: (all[nodeId] || []).map((n) => n.id === noteId ? { ...n, active: n.active === false } : n) }));
  const remove = (noteId) => setNotes((all) => ({ ...all, [nodeId]: (all[nodeId] || []).filter((n) => n.id !== noteId) }));
  const addText = (text) => {
    const t = (text || '').trim();if (!t) return;
    setNotes((all) => ({ ...all, [nodeId]: [...(all[nodeId] || []), window.ConstructorData.note({ author: 'Super-admin', role: 'Super-admin', color: '#e8793a', text: t, ts: 'сейчас', active: true })] }));
    setDraft('');setQ('');setOpen(false);setTab('lib');
  };
  const snippet = (txt) => {const t = (txt || '').trim();return t.length > 32 ? t.slice(0, 32) + '…' : t || 'без текста';};
  const usedTexts = list.map((n) => (n.text || '').trim());
  const tpl = q ? NOTE_TEMPLATES.filter((t) => t.title.toLowerCase().includes(q.toLowerCase()) || t.text.toLowerCase().includes(q.toLowerCase())) : NOTE_TEMPLATES;

  return (
    <section style={insStyles.sec}>
      <div style={insStyles.secLabel}>Заметки{list.length ? ' (' + list.length + ')' : ''}</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
        {list.map((n) => {
          const on = n.active !== false;
          return (
            <span key={n.id} title={(on ? 'Активна' : 'Выключена') + ' · ' + (n.text || '')} style={{
              display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 6px 4px 10px', borderRadius: 980, fontSize: 12, fontWeight: 600, maxWidth: '100%',
              background: on ? n.color + '18' : '#f0ece5', color: on ? n.color : '#a89e92', border: on ? '1px solid ' + n.color + '44' : '1px solid transparent'
            }}>
              <button onClick={() => toggle(n.id)} title={on ? 'Деактивировать' : 'Активировать'} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, color: 'inherit', padding: 0, maxWidth: 200 }}>
                <span style={{ width: 6, height: 6, borderRadius: '50%', background: on ? n.color : '#bcb3a7', flexShrink: 0 }}></span>
                <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{snippet(n.text)}</span>
              </button>
              <button onClick={() => remove(n.id)} title="Удалить" style={{ display: 'inline-flex', width: 16, height: 16, alignItems: 'center', justifyContent: 'center', borderRadius: '50%', background: 'none', border: 'none', cursor: 'pointer', color: 'currentColor', opacity: .6, padding: 0 }}>{Icon.x}</button>
            </span>);

        })}
        {/* + добавить — попап */}
        <div ref={wrapRef} style={{ position: 'relative' }}>
          <button onClick={() => {setOpen((o) => !o);setTab('lib');}} style={{
            display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px 5px 9px', borderRadius: 980, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, transition: 'all .12s',
            background: open ? '#fff0e6' : '#f0ece5', border: '.5px solid ' + (open ? '#e8793a40' : 'transparent'), color: open ? '#e8793a' : '#6b6259'
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
            заметка
          </button>
          {open &&
          <div style={{ position: 'absolute', top: 'calc(100% + 6px)', left: 0, width: 320, zIndex: 80, background: '#fff', borderRadius: 14, border: '.5px solid #e4ddd2', boxShadow: '0 14px 40px rgba(0,0,0,.16)', overflow: 'hidden', animation: 'popIn .13s cubic-bezier(.32,.72,0,1)' }}>
              {/* tabs */}
              <div style={{ display: 'flex', gap: 2, padding: '8px 10px 0' }}>
                {[{ v: 'lib', l: 'Из шаблонов' }, { v: 'new', l: 'Создать новую' }].map((o) =>
              <button key={o.v} onClick={() => setTab(o.v)} style={{
                flex: 1, padding: '6px 10px', borderRadius: 8, fontSize: 12, fontWeight: 600, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
                background: tab === o.v ? '#1a1714' : '#f0ece5', color: tab === o.v ? '#fff' : '#8a817a'
              }}>{o.l}</button>
              )}
              </div>
              {tab === 'lib' ?
            <div>
                  <div style={{ padding: '9px 11px' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 7, background: '#f0ece5', borderRadius: 9, padding: '6px 10px' }}>
                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#8a817a" strokeWidth="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
                      <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск шаблона" style={{ border: 'none', outline: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 12.5, flex: 1 }} />
                    </div>
                  </div>
                  <div style={{ maxHeight: 240, overflowY: 'auto', padding: '0 5px 5px' }}>
                    {tpl.map((t) => {
                  const used = usedTexts.includes(t.text.trim());
                  return (
                    <button key={t.id} onClick={() => !used && addText(t.text)} disabled={used} style={{
                      width: '100%', display: 'flex', alignItems: 'flex-start', gap: 9, padding: '8px 9px', borderRadius: 9, border: 'none', cursor: used ? 'default' : 'pointer', fontFamily: 'inherit', textAlign: 'left', opacity: used ? 0.5 : 1, background: 'transparent'
                    }}
                    onMouseEnter={(e) => {if (!used) e.currentTarget.style.background = '#fff7f0';}}
                    onMouseLeave={(e) => {e.currentTarget.style.background = 'transparent';}}>
                          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#e8793a" strokeWidth="2" style={{ marginTop: 2, flexShrink: 0 }}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
                          <span style={{ flex: 1, minWidth: 0 }}>
                            <span style={{ display: 'block', fontSize: 13, fontWeight: 600, color: '#1a1714' }}>{t.title}</span>
                            <span style={{ display: 'block', fontSize: 11.5, color: '#8a817a', lineHeight: 1.4, marginTop: 1 }}>{t.text}</span>
                          </span>
                          {used && <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5aad6e" strokeWidth="2.5" style={{ flexShrink: 0, marginTop: 2 }}><polyline points="20 6 9 17 4 12"></polyline></svg>}
                        </button>);

                })}
                    {tpl.length === 0 && <div style={{ padding: '16px 12px', textAlign: 'center', color: '#a89e92', fontSize: 12.5 }}>Не найдено</div>}
                  </div>
                </div> :

            <div style={{ padding: '10px 11px 11px' }}>
                  <textarea autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} rows={4} placeholder="Текст заметки / инструкции для этого узла…"
              style={{ width: '100%', resize: 'vertical', fontFamily: 'inherit', fontSize: 13, lineHeight: 1.5, color: '#1a1714', background: '#f0ece5', border: '.5px solid #e4ddd2', borderRadius: 10, padding: '9px 11px', outline: 'none' }} />
                  <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 9 }}>
                    <button onClick={() => addText(draft)} disabled={!draft.trim()} style={{ ...ctorStyles.btnPrimary, opacity: draft.trim() ? 1 : 0.45, padding: '6px 14px', fontSize: 12.5 }}>Добавить заметку</button>
                  </div>
                </div>
            }
            </div>
          }
        </div>
      </div>
    </section>);

}

/* ============ BOM — «Материалы по умолчанию» на узле (C-4) ============ */
const QTY_FORMULAS = [
{ v: 'fixed', l: 'Фикс. количество' },
{ v: 'workVolume', l: '= объём работы' },
{ v: 'workVolume*1.1', l: 'объём × 1.1 (запас 10%)' },
{ v: 'workVolume*1.2', l: 'объём × 1.2 (запас 20%)' },
{ v: 'workVolume*0.9', l: 'объём × 0.9' }];


function BomSection({ node, bindings, setBindings, materialsLib }) {
  const MDx = window.MaterialsData;
  if (!MDx || !setBindings) return null;
  const list = bindings && bindings[node.id] || [];
  const [open, setOpen] = inUseState(false);
  const [q, setQ] = inUseState('');
  const wrapRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => {if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);};
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);

  const setRows = (rows) => setBindings((all) => ({ ...all, [node.id]: rows }));
  const addMaterial = (m) => {setRows([...list, MDx.bind(m.id, {})]);setOpen(false);setQ('');};
  const addManual = () => setRows([...list, { id: MDx.uid('bom'), materialId: null, manualName: 'Ручная позиция', defaultQty: 1, unit: 'шт', qtyFormula: 'fixed', consumptionRate: null, isRequired: true, isManual: true }]);
  const patchRow = (id, patch) => setRows(list.map((b) => b.id === id ? { ...b, ...patch } : b));
  const removeRow = (id) => setRows(list.filter((b) => b.id !== id));
  const usedIds = new Set(list.map((b) => b.materialId));
  const pick = (materialsLib || []).filter((m) => !q || (m.name + ' ' + m.sku).toLowerCase().includes(q.toLowerCase()));
  const totalHint = list.reduce((a, b) => {const m = MDx.materialById(materialsLib, b.materialId);return a + (m ? MDx.costHintFor(m, b.defaultQty) : 0);}, 0);

  return (
    <section style={insStyles.sec}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
        <div style={insStyles.secLabel}>Материалы по умолчанию{list.length ? ' (' + list.length + ')' : ''}</div>
        {totalHint > 0 && <span style={{ fontSize: 11, fontWeight: 700, color: '#5aad6e', fontVariantNumeric: 'tabular-nums' }}>≈ {totalHint.toLocaleString('ru-RU')} ₽</span>}
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {list.map((b) => {
          const m = b.isManual ? null : MDx.materialById(materialsLib, b.materialId);
          const hint = m ? MDx.costHintFor(m, b.defaultQty) : 0;
          return (
            <div key={b.id} style={{ background: '#f0ece5', borderRadius: 10, padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 7 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                <span style={{ fontSize: 12.5 }}>📦</span>
                {b.isManual ?
                <input value={b.manualName || ''} onChange={(e) => patchRow(b.id, { manualName: e.target.value })} placeholder="Название позиции"
                style={{ flex: 1, minWidth: 0, border: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600, color: '#1a1714', outline: 'none' }} /> :
                <span style={{ flex: 1, minWidth: 0, fontSize: 12.5, fontWeight: 600, color: m ? '#1a1714' : '#f04e62', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m ? m.name : '⚠ позиция удалена'}</span>}
                {m && window.MaterialKindChip && React.createElement(window.MaterialKindChip, { kind: m.materialKind, small: true })}
                {b.isManual && <span style={{ fontSize: 10, fontWeight: 700, color: '#8a817a', background: '#fff', padding: '1px 6px', borderRadius: 980 }}>ручная</span>}
                <button onClick={() => removeRow(b.id)} style={{ ...ctorStyles.iconBtnSm, width: 20, height: 20, color: '#bcb3a7' }} title="Убрать">{Icon.x}</button>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, background: '#fff', border: '.5px solid #e4ddd2', borderRadius: 8, padding: '3px 8px' }}>
                  <input type="number" min={0} step={0.5} value={b.defaultQty} onChange={(e) => patchRow(b.id, { defaultQty: Math.max(0, +e.target.value || 0) })}
                  style={{ width: 44, border: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 700, color: '#1a1714', textAlign: 'right', outline: 'none', fontVariantNumeric: 'tabular-nums' }} />
                  <input value={b.unit} onChange={(e) => patchRow(b.id, { unit: e.target.value })} style={{ width: 34, border: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 11.5, color: '#8a817a', outline: 'none' }} />
                </span>
                <select value={b.qtyFormula || 'fixed'} onChange={(e) => patchRow(b.id, { qtyFormula: e.target.value })}
                style={{ fontFamily: 'inherit', fontSize: 11.5, color: '#6b6259', background: '#fff', border: '.5px solid #e4ddd2', borderRadius: 8, padding: '4px 6px', outline: 'none', cursor: 'pointer', maxWidth: 150 }}>
                  {QTY_FORMULAS.map((f) => <option key={f.v} value={f.v}>{f.l}</option>)}
                </select>
                <button onClick={() => patchRow(b.id, { isRequired: !b.isRequired })} title="Обязательность"
                style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 9px', borderRadius: 980, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 10.5, fontWeight: 700,
                  background: b.isRequired ? '#fdecec' : '#fff', color: b.isRequired ? '#f04e62' : '#a89e92' }}>
                  {b.isRequired ? 'обяз.' : 'опц.'}
                </button>
                {hint > 0 && <span style={{ marginLeft: 'auto', fontSize: 11, color: '#8a817a', fontVariantNumeric: 'tabular-nums' }}>≈ {hint.toLocaleString('ru-RU')} ₽</span>}
              </div>
            </div>);

        })}
      </div>

      <div style={{ display: 'flex', gap: 6, position: 'relative' }} ref={wrapRef}>
        <button onClick={() => {setOpen((o) => !o);setQ('');}} style={{ ...ctorStyles.btnDashed, flex: 1, padding: '8px', fontSize: 12.5 }}>{Icon.plus} Из библиотеки</button>
        <button onClick={addManual} style={{ ...ctorStyles.btnDashed, width: 'auto', padding: '8px 12px', fontSize: 12.5, color: '#8a817a', borderColor: '#e4ddd2' }} title="Ручная строка">+ Строка</button>
        {open &&
        <div style={{ position: 'absolute', top: 'calc(100% + 6px)', left: 0, right: 0, zIndex: 90, background: '#fff', borderRadius: 12, border: '.5px solid #e4ddd2', boxShadow: '0 14px 40px rgba(0,0,0,.16)', overflow: 'hidden', animation: 'popIn .13s cubic-bezier(.32,.72,0,1)' }}>
            <div style={{ padding: '9px 11px', borderBottom: '.5px solid #f0ece5' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 7, background: '#f0ece5', borderRadius: 9, padding: '6px 10px' }}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#8a817a" strokeWidth="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
                <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск позиции" style={{ border: 'none', outline: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 12.5, flex: 1 }} />
              </div>
            </div>
            <div style={{ maxHeight: 260, overflowY: 'auto', padding: 5 }}>
              {pick.map((m) => {
              const dup = usedIds.has(m.id);
              return (
                <button key={m.id} onClick={() => !dup && addMaterial(m)} disabled={dup} style={{
                  width: '100%', display: 'flex', alignItems: 'center', gap: 9, padding: '8px 9px', borderRadius: 9, border: 'none', cursor: dup ? 'default' : 'pointer', fontFamily: 'inherit', textAlign: 'left', opacity: dup ? 0.45 : 1, background: 'transparent'
                }}
                onMouseEnter={(e) => {if (!dup) e.currentTarget.style.background = '#fff7f0';}}
                onMouseLeave={(e) => {e.currentTarget.style.background = 'transparent';}}>
                    <span style={{ fontSize: 13 }}>📦</span>
                    <span style={{ flex: 1, minWidth: 0 }}>
                      <span style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#1a1714', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.name}</span>
                      <span style={{ display: 'block', fontSize: 11, color: '#8a817a' }}>{(MDx.subtypeById(m.subtype) || {}).name} · {(m.unitPrice || 0).toLocaleString('ru-RU')} ₽/{m.unit}</span>
                    </span>
                    {dup && <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#5aad6e" strokeWidth="3" style={{ flexShrink: 0 }}><polyline points="20 6 9 17 4 12"></polyline></svg>}
                  </button>);

            })}
              {pick.length === 0 && <div style={{ padding: '16px', textAlign: 'center', color: '#a89e92', fontSize: 12.5 }}>Ничего не найдено</div>}
            </div>
          </div>
        }
      </div>
      {list.length === 0 && <div style={insStyles.hint}>Материалы, которые система предложит в смете на воронке для этого узла.</div>}
    </section>);

}

function normalizeLabeledOptions(rows) {
  return rows
    .filter((r) => (r.l || r.v || '').trim())
    .map((r) => {
      const l = (r.l || r.v || '').trim();
      const v = (r.v || r.l || '').trim();
      return v === l ? l : { v, l };
    });
}

function OptionsListEditor({ label, options, onChange, placeholder, hint, labeled, compact, className, style }) {
  const toLabeledRows = (opts) => (opts || []).map((o) => {
    if (typeof o === 'object' && o) return { l: o.l || '', v: o.v || '' };
    const s = String(o ?? '');
    return { l: s, v: s };
  });
  const toSimpleRows = (opts) => (opts || []).map(String);
  const optsKey = JSON.stringify(options || []);

  const [rows, setRows] = React.useState(() => (labeled ? toLabeledRows(options) : toSimpleRows(options)));
  React.useEffect(() => {
    setRows(labeled ? toLabeledRows(options) : toSimpleRows(options));
  }, [optsKey, labeled]);

  const emitLabeled = (next) => {
    setRows(next);
    onChange(normalizeLabeledOptions(next));
  };
  const emitSimple = (next) => {
    setRows(next);
    onChange(next.map((s) => s.trim()).filter(Boolean));
  };

  const addRow = () => {
    if (labeled) setRows((r) => [...r, { l: '', v: '' }]);
    else setRows((r) => [...r, '']);
  };

  const removeRow = (idx) => {
    if (labeled) {
      const next = rows.filter((_, i) => i !== idx);
      emitLabeled(next);
    } else {
      const next = rows.filter((_, i) => i !== idx);
      emitSimple(next);
    }
  };

  const rootCls = 'opt-list-editor' + (compact ? ' is-compact' : '') + (className ? ' ' + className : '');

  return (
    <div className={rootCls} style={style}>
      {label && <div style={insStyles.label}>{label}</div>}
      <div className="opt-list-editor-list">
        {labeled ? rows.map((row, i) => (
          <div key={i} className="opt-list-editor-row">
            <input className="opt-list-editor-input" value={row.l} onChange={(e) => {
              const next = rows.map((r, j) => (j === i ? { ...r, l: e.target.value } : r));
              emitLabeled(next);
            }} placeholder={placeholder || 'Подпись'} />
            <input className="opt-list-editor-input opt-list-editor-input--key" value={row.v} onChange={(e) => {
              const next = rows.map((r, j) => (j === i ? { ...r, v: e.target.value } : r));
              emitLabeled(next);
            }} placeholder="Ключ" title="Ключ (необязательно)" />
            <button type="button" className="opt-list-editor-remove" onClick={() => removeRow(i)} aria-label="Удалить вариант">{Icon.x}</button>
          </div>
        )) : rows.map((val, i) => (
          <div key={i} className="opt-list-editor-row">
            <input className="opt-list-editor-input" value={val} onChange={(e) => {
              const next = [...rows];
              next[i] = e.target.value;
              emitSimple(next);
            }} placeholder={placeholder || 'Вариант'} />
            <button type="button" className="opt-list-editor-remove" onClick={() => removeRow(i)} aria-label="Удалить вариант">{Icon.x}</button>
          </div>
        ))}
      </div>
      <button type="button" className="opt-list-editor-add" onClick={addRow} style={{ ...ctorStyles.btnDashed, marginTop: rows.length ? 4 : 0 }}>{Icon.plus} Добавить вариант</button>
      {hint && <span style={insStyles.hint}>{hint}</span>}
    </div>
  );
}

Object.assign(window, { Inspector, insStyles, VIS_ROLES, FText, FNum, FToggle, FSeg, FSelect, VisibilityFlags, ChecklistAttachSection, OptionsChipsSection, MeasureAttachSection, NoteChipsSection, NOTE_TEMPLATES, BomSection, InspectorSectionNav, InspectorFooterBand, InspectorFooterBlock, InspectorEmptyState, InspectorShell, InspectorHead, InspectorSection, InspectorFieldGrid, InspectorCallout, InspectorMetric, InspectorDrawer, InclusionFields, ConditionSection, DependsOnPicker, OptionsListEditor, normalizeLabeledOptions });

/* ============================================================
   InspectorInline — multi-column dense, visually unified with card
   ============================================================ */
const { useState: ilUseState } = React;

function InspectorInline({ node, patch, roles, notes, setNotes, onTabSwitch, onClose, checklists, measures, options, bindings, setBindings, materialsLib, catalog, focusSection }) {
  if (!node) return null;
  const t = node._type || node.type || 'task';
  const role = roles.find((r) => r.id === node.role);
  const noteCnt = (notes && notes[node.id] || []).length;
  const isStage = t === 'stage';
  const cat = catalog || (window.ConstructorData && window.ConstructorData.loadCatalog ? window.ConstructorData.loadCatalog() : []);
  const condRef = React.useRef(null);
  React.useEffect(() => {
    if (focusSection === 'condition' && condRef.current) condRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }, [focusSection, node.id]);

  return (
    <InspectorShell
      compact={!isStage}
      head={
        <InspectorHead
          compact
          typeLabel={NODE_TYPE_LABEL[t] || t}
          typeColor={isStage ? node.color || '#8a817a' : t === 'task' ? '#e8793a' : '#8a817a'}
          roleAbbr={role && role.abbr}
          roleColor={role && role.color}
          title={node.name}
          onClose={onClose}
        />
      }
      footer={
        <React.Fragment>
          <InspectorFooterBand title="Вложения и доступ">
            <InspectorFooterBlock title="Чек-лист">
              <ChecklistAttachSection nodeId={node.id} node={node} patch={patch} checklists={checklists || []} />
            </InspectorFooterBlock>
            {setBindings && (
              <InspectorFooterBlock title="Материалы по умолчанию">
                <BomSection node={node} bindings={bindings} setBindings={setBindings} materialsLib={materialsLib || []} />
              </InspectorFooterBlock>
            )}
            <InspectorFooterBlock title="Шаблон замера">
              <MeasureAttachSection nodeId={node.id} node={node} patch={patch} measures={measures || []} nodeType={t} />
            </InspectorFooterBlock>
            <InspectorFooterBlock title={'Заметки' + (noteCnt ? ' · ' + noteCnt : '')} className="ins-footer-block--wide">
              <NoteChipsSection nodeId={node.id} node={node} notes={notes} setNotes={setNotes} onTabSwitch={onTabSwitch} />
            </InspectorFooterBlock>
            <InspectorFooterBlock title="Видимость по ролям" className="ins-footer-block--wide">
              <VisibilityFlags node={node} patch={patch} />
            </InspectorFooterBlock>
          </InspectorFooterBand>
          <div style={{ borderTop: '1px solid var(--border-subtle)', padding: '9px 13px 11px', background: 'transparent' }}>
            <OptionsChipsSection nodeId={node.id} node={node} patch={patch} options={options || []} />
          </div>
        </React.Fragment>
      }
    >
      <div className="ins-inline-grid">
        <InspectorSection title="Основное">
          {(t === 'task' || t === 'work') && <FSelect label="Роль" value={node.role} onChange={(v) => patch({ role: v })} options={roles.map((r) => ({ v: r.id, l: r.name }))} placeholder="—" />}
          {(t === 'task' || t === 'work') && <FSeg label="Уровень" value={node.level || ''} onChange={(v) => patch({ level: v })} options={[{ v: '', l: 'любой' }, { v: 'elite', l: 'Элита' }, { v: 'pro', l: 'Профи' }]} />}
          {isStage && <FText label="Заметка" value={node.note} onChange={(v) => patch({ note: v })} placeholder="Видна в дереве" />}
        </InspectorSection>

        {(t === 'stage' || t === 'task' || t === 'work') && (
          <InspectorSection title="Включение">
            <InclusionFields node={node} patch={patch} />
          </InspectorSection>
        )}

        {(t === 'stage' || t === 'task' || t === 'work') && (
          <div ref={condRef}>
            <ConditionSection node={node} patch={patch} catalog={cat} selfId={node.id} />
          </div>
        )}

        <InspectorSection title="Сроки и объём">
          <InspectorFieldGrid cols={2}>
            <FNum label="Дни" value={node.days} onChange={(v) => patch({ days: v })} suffix="д" />
            <FNum label="Тех. простой" value={t === 'task' ? node.pause : node.techPause} onChange={(v) => patch(t === 'task' ? { pause: v } : { techPause: v })} suffix="д" />
            {(t === 'task' || t === 'work') && (
              <React.Fragment>
                <div style={{ gridColumn: '1 / -1' }}><FSeg label="Единица" value={node.unit || 'm2'} onChange={(v) => patch({ unit: v })} options={[{ v: 'm2', l: 'м²' }, { v: 'lm', l: 'м' }, { v: 'pcs', l: 'шт' }]} /></div>
                <FNum label="Объём" value={node.volume} onChange={(v) => patch({ volume: v })} suffix={node.unit === 'lm' ? 'м' : node.unit === 'pcs' ? 'шт' : 'м²'} />
                <FNum label="Часы/ед." value={node.hoursPerUnit} onChange={(v) => patch({ hoursPerUnit: v })} suffix="ч" step={0.5} />
              </React.Fragment>
            )}
          </InspectorFieldGrid>
        </InspectorSection>

        {(t === 'task' || t === 'work') && (() => {
          const subs = window.loadSubs ? window.loadSubs() : [];
          if (!subs.length) return null;
          const serviceSubs = subs.filter((s) => (s.services || []).some((v) => v.kind === 'service'));
          if (!serviceSubs.length) return null;
          const on = !!node.subcontractorOn;
          const linked = subs.find((s) => s.id === node.subSupplierId);
          const ops = linked ? (linked.services || []).filter((v) => v.kind === 'service') : [];
          const nm = (node.name || '').toLowerCase();
          const suggest = serviceSubs.find((s) => (s.services || []).some((v) => v.kind === 'service' && (
            nm.includes('окн') && v.label.toLowerCase().includes('окн') ||
            nm.includes('мусор') && v.label.toLowerCase().includes('мусор') ||
            nm.includes('конд') && v.label.toLowerCase().includes('сплит'))));
          return (
            <InspectorSection title="Субподрядчик">
              <FToggle label="Включить субподрядчика" value={on}
                onChange={(v) => patch({ subcontractorOn: v, subSupplierId: v ? node.subSupplierId || suggest && suggest.id || serviceSubs[0].id : null })}
                hint="Работу выполняет внешняя компания" />
              {on && (
                <React.Fragment>
                  {suggest && !node.subSupplierId && (
                    <InspectorCallout variant="success">Система предлагает: <strong>{suggest.name}</strong> — подходит по типу работы.</InspectorCallout>
                  )}
                  <FSelect label="Компания" value={node.subSupplierId || ''} onChange={(v) => patch({ subSupplierId: v })}
                    options={serviceSubs.map((s) => ({ v: s.id, l: s.name }))} placeholder="— выбрать —" />
                  {linked && ops.length > 0 && (
                    <InspectorCallout variant="warn">
                      <div style={{ fontWeight: 700, marginBottom: 7 }}>В смету подтянутся операции</div>
                      {ops.map((v) => {
                        const cl = v.cost + Math.round(v.cost * (v.margin || 0) / 100);
                        return (
                          <div key={v.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', fontSize: 12.5 }}>
                            <span style={{ flex: 1, minWidth: 0 }}>{v.label}</span>
                            <span className="t-num" style={{ fontWeight: 700 }}>{cl.toLocaleString('ru-RU')} ₽</span>
                          </div>
                        );
                      })}
                    </InspectorCallout>
                  )}
                </React.Fragment>
              )}
            </InspectorSection>
          );
        })()}

        {(() => {
          const nodeClient = (n) => { const c = +n.cost || 0, m = n.margin == null ? 30 : +n.margin; return c + Math.round(c * m / 100); };
          const nodeCost = (n) => +n.cost || 0;
          const hasSubs = t === 'task' && (node.sub || []).length > 0;
          if (isStage || hasSubs) {
            const kids = isStage ?
              (node.tasks || []).map((tk) => tk.sub && tk.sub.length ? { cost: tk.sub.reduce((b, w) => b + nodeCost(w), 0), client: tk.sub.reduce((b, w) => b + nodeClient(w), 0) } : { cost: nodeCost(tk), client: nodeClient(tk) }) :
              (node.sub || []).map((w) => ({ cost: nodeCost(w), client: nodeClient(w) }));
            const sumCost = kids.reduce((a, k) => a + k.cost, 0);
            const sumClient = kids.reduce((a, k) => a + k.client, 0);
            return (
              <InspectorSection title={isStage ? 'Стоимость этапа' : 'Стоимость задачи'}>
                <InspectorMetric label="Себестоимость · Σ" value={sumCost.toLocaleString('ru-RU') + ' ₽'} />
                <InspectorMetric label="Цена клиенту · Σ" value={sumClient.toLocaleString('ru-RU') + ' ₽'} accent />
                <div style={insStyles.hint}>Формируется из {isStage ? 'задач и подзадач' : 'подзадач'}.</div>
              </InspectorSection>
            );
          }
          const c = +node.cost || 0, m = node.margin == null ? 30 : +node.margin, tax = +node.tax || 0;
          const marginRub = Math.round(c * m / 100), client = c + marginRub, withTax = Math.round(client * (1 + tax / 100));
          return (
            <InspectorSection title="Стоимость работ">
              <InspectorFieldGrid cols={2}>
                <FNum label="Себестоимость" value={node.cost} onChange={(v) => patch({ cost: v })} suffix="₽" step={100} />
                <FNum label="Цена/ед." value={node.unitPrice} onChange={(v) => patch({ unitPrice: v })} suffix="₽" step={50} />
                <FNum label={'Маржа (+' + marginRub.toLocaleString('ru-RU') + ' ₽)'} value={node.margin == null ? 30 : node.margin} onChange={(v) => patch({ margin: v })} suffix="%" step={1} />
                <FNum label="Налог" value={node.tax == null ? 0 : node.tax} onChange={(v) => patch({ tax: v })} suffix="%" step={1} />
              </InspectorFieldGrid>
              <InspectorMetric label="Цена клиенту" value={client.toLocaleString('ru-RU') + ' ₽'} accent />
              <InspectorMetric label={'с налогом · индекс ' + (node.costIndex == null ? 5 : node.costIndex) + '%/мес'} value={withTax.toLocaleString('ru-RU') + ' ₽'} />
            </InspectorSection>
          );
        })()}
      </div>
    </InspectorShell>
  );
}

Object.assign(window, { InspectorInline });