/* calc2-tree.jsx — дерево сметы в режиме СБОРКИ (mirror «Конструктор / Смета»).
   Структура и правила — read-only (из родительского каталога). Сотрудник только
   собирает: include / qty / цена / picker материалов / manual line / решение клиента.
   Права — Calc2.can(role, action, status). Видимость строки — Calc2.visibleTo. */
(function () {
  const C = window.Calc2;
  const HV = () => window.HierarchyView;
  const HVis = (props) => (window.HierarchyVis ? window.HierarchyVis(props) : props.children);
  const { Ic } = window;
  const { useState, useEffect, useMemo } = React;

  const DEP_TYPE_META = {
    stage: { label: 'Этап', color: 'var(--info)' },
    task: { label: 'Задача', color: 'var(--primary)' },
    work: { label: 'Подзадача', color: '#a1761f' },
  };

  const U = (u) => C.unitLabel(u);

  /* ── read-only «правила из конструктора» ── */
  function RolePill({ roleId }) {
    const r = C.roleInfo(roleId);
    if (!r) return null;
    return <span className="cc-pill role ro" title={'Исполнитель (из каталога): ' + r.name} style={{ background: r.color }}>{r.abbr}</span>;
  }
  function LevelPill({ level }) {
    if (!level) return null;
    const m = level === 'elite' ? ['Элита 0–1', 'elite'] : level === 'pro' ? ['Профи 2–3', 'pro'] : null;
    if (!m) return null;
    return <span className={'cc-pill lvl ro ' + m[1]} title="Требуемый уровень (из каталога)">★ {m[0]}</span>;
  }
  function IncPill({ inc, cond, dependsOn }) {
    if (inc === 'optional') return <span className="cc-pill inc-opt ro" title="Опциональная работа — можно включить/исключить">опц.</span>;
    if (inc === 'conditional') {
      const deps = dependsOn || [];
      if (deps.length) {
        const first = (deps[0].path || deps[0].nodeId || '').split(' › ').pop();
        return <span className="cc-pill inc-cond ro" title={'Условная · зависит от: ' + deps.map((d) => d.path || d.nodeId).join(', ')}>усл. · {first}{deps.length > 1 ? ` (+${deps.length - 1})` : ''}</span>;
      }
      if (cond) return <span className="cc-pill inc-cond ro dep-badge--legacy" title={'Устаревшее условие: ' + cond}>устар. · {cond}</span>;
      return <span className="cc-pill inc-cond ro" title="Условная работа">усл.</span>;
    }
    return null;
  }

  function CalcDepPicker({ catalog, selfId, onAdd, excludeIds }) {
    const [q, setQ] = useState('');
    const [phaseId, setPhaseId] = useState(null);
    const [stageId, setStageId] = useState(null);
    const [typeFilter, setTypeFilter] = useState(null);
    const CD = C.CD || window.ConstructorData;
    const ql = q.trim().toLowerCase();

    const eligible = useMemo(() => {
      const exSet = new Set(excludeIds || []);
      return (CD.flattenCatalog ? CD.flattenCatalog(catalog) : []).filter((row) => {
        if (row.type === 'phase') return false;
        if (row.node.id === selfId || exSet.has(row.node.id)) return false;
        if (CD.isDescendantOf && CD.isDescendantOf(catalog, selfId, row.node.id)) return false;
        if (phaseId && row.phase && row.phase.id !== phaseId) return false;
        if (stageId && row.stage && row.stage.id !== stageId) return false;
        if (typeFilter && row.type !== typeFilter) return false;
        const path = CD.buildNodePath ? CD.buildNodePath(catalog, row.node.id) : (row.node.name || '');
        const label = (row.node.name || '') + ' ' + path;
        return !ql || label.toLowerCase().includes(ql);
      });
    }, [catalog, selfId, excludeIds, phaseId, stageId, typeFilter, q, CD, ql]);

    const groups = useMemo(() => {
      const map = new Map();
      eligible.forEach((row) => {
        const key = (row.phase && row.phase.id) + '::' + (row.stage && row.stage.id);
        if (!map.has(key)) map.set(key, { phase: row.phase, stage: row.stage, rows: [] });
        map.get(key).rows.push(row);
      });
      return Array.from(map.values());
    }, [eligible]);

    const counts = useMemo(() => {
      const exSet = new Set(excludeIds || []);
      const all = (CD.flattenCatalog ? CD.flattenCatalog(catalog) : []).filter((row) => {
        if (row.type === 'phase') return false;
        if (row.node.id === selfId || exSet.has(row.node.id)) return false;
        if (CD.isDescendantOf && CD.isDescendantOf(catalog, selfId, row.node.id)) return false;
        return true;
      });
      const byPhase = {}, byStage = {}, byType = { stage: 0, task: 0, work: 0 };
      all.forEach((row) => {
        if (row.phase) byPhase[row.phase.id] = (byPhase[row.phase.id] || 0) + 1;
        if (row.stage) byStage[row.stage.id] = (byStage[row.stage.id] || 0) + 1;
        if (byType[row.type] != null) byType[row.type] += 1;
      });
      return { total: all.length, byPhase, byStage, byType };
    }, [catalog, selfId, excludeIds, CD]);

    const pickRow = (row) => {
      const path = CD.buildNodePath ? CD.buildNodePath(catalog, row.node.id) : row.node.name;
      onAdd({ nodeId: row.node.id, nodeType: row.type, path });
    };

    const selectPhase = (id) => { setPhaseId(id); setStageId(null); setTypeFilter(null); };
    const selectStage = (phId, stId) => { setPhaseId(phId); setStageId(stId); setTypeFilter(null); };
    const selectAll = () => { setPhaseId(null); setStageId(null); setTypeFilter(null); };

    return (
      <div className="dep-picker dep-picker--lib">
        <div className="dep-picker-search dep-picker-search--top">
          <Ic n="search" s={14} c="var(--text-tertiary)" />
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск этапа, задачи, подзадачи…" autoFocus />
        </div>
        <div className="dep-picker-layout">
          <aside className="dep-picker-side">
            <button type="button" className={'dep-picker-nav' + (!phaseId && !stageId && !typeFilter ? ' on' : '')} onClick={selectAll}>
              <span>Весь каталог</span>
              <span className="dep-picker-nav-n">{counts.total}</span>
            </button>
            <div className="dep-picker-nav-sep">Вид работ</div>
            {['stage', 'task', 'work'].map((t) => {
              const m = DEP_TYPE_META[t];
              const on = typeFilter === t && !phaseId && !stageId;
              return (
                <button key={t} type="button" className={'dep-picker-nav dep-picker-nav--type' + (on ? ' on' : '')} onClick={() => { setTypeFilter(on ? null : t); setPhaseId(null); setStageId(null); }}>
                  <span className="dep-picker-type-dot" style={{ background: m.color }} />
                  <span>{m.label}</span>
                  <span className="dep-picker-nav-n">{counts.byType[t] || 0}</span>
                </button>
              );
            })}
            <div className="dep-picker-nav-sep">Фазы и этапы</div>
            {(catalog || []).map((ph) => (
              <div key={ph.id} className="dep-picker-nav-group">
                <button type="button" className={'dep-picker-nav' + (phaseId === ph.id && !stageId ? ' on' : '')} onClick={() => selectPhase(ph.id)}>
                  <span className="dep-picker-nav-name">{ph.name}</span>
                  <span className="dep-picker-nav-n">{counts.byPhase[ph.id] || 0}</span>
                </button>
                <div className="dep-picker-nav-sub">
                  {(ph.stages || []).map((st) => (
                    <button key={st.id} type="button" className={'dep-picker-nav dep-picker-nav--sub' + (stageId === st.id ? ' on' : '')} onClick={() => selectStage(ph.id, st.id)}>
                      <span className="dep-picker-stage-dot" style={{ background: st.color || 'var(--text-tertiary)' }} />
                      <span className="dep-picker-nav-name">{st.name}</span>
                      <span className="dep-picker-nav-n">{counts.byStage[st.id] || 0}</span>
                    </button>
                  ))}
                </div>
              </div>
            ))}
          </aside>
          <div className="dep-picker-main">
            <div className="dep-picker-toolbar">
              <span className="dep-picker-count">{eligible.length} {eligible.length === 1 ? 'позиция' : eligible.length < 5 ? 'позиции' : 'позиций'}</span>
            </div>
            {eligible.length === 0 ? (
              <div className="dep-picker-empty">Ничего не найдено. Измените фильтр или поиск.</div>
            ) : (
              groups.map((g) => (
                <section key={(g.phase && g.phase.id) + '::' + (g.stage && g.stage.id)} className="dep-picker-group">
                  <header className="dep-picker-group-h">
                    <span className="dep-picker-group-phase">{g.phase && g.phase.name}</span>
                    <span className="dep-picker-group-stage">{g.stage && g.stage.name}</span>
                    <span className="dep-picker-group-n">{g.rows.length}</span>
                  </header>
                  <div className="dep-picker-list">
                    {g.rows.map((row) => {
                      const tm = DEP_TYPE_META[row.type] || DEP_TYPE_META.task;
                      const parent = row.type === 'work' && row.task ? row.task.name : (row.type === 'task' ? null : null);
                      return (
                        <button key={row.node.id} type="button" className="dep-picker-item" onClick={() => pickRow(row)}>
                          <span className="dep-picker-type" style={{ color: tm.color }}>{tm.label}</span>
                          <span className="dep-picker-item-body">
                            <span className="dep-picker-item-name">{row.node.name}</span>
                            {parent && <span className="dep-picker-item-meta">{parent}</span>}
                          </span>
                          <Ic n="plus" s={14} c="var(--text-tertiary)" />
                        </button>
                      );
                    })}
                  </div>
                </section>
              ))
            )}
          </div>
        </div>
      </div>
    );
  }

  function DepChips({ deps, customized, onRemove, canEdit }) {
    if (!deps || !deps.length) return null;
    return (
      <span className="cc-dep-chips">
        {deps.map((d) => (
          <span key={d.nodeId} className={'cc-dep-chip' + (customized ? ' customized' : '')} title={d.path || d.nodeId}>
            {(d.path || d.nodeId).split(' › ').pop()}
            {canEdit && onRemove && <button type="button" onClick={() => onRemove(d.nodeId)} title="Убрать зависимость"><Ic n="x" s={10} /></button>}
          </span>
        ))}
      </span>
    );
  }
  function MoneyChip({ v, strong, title }) {
    if (!v) return null;
    return <span className={'cc-money' + (strong ? ' strong' : '')} title={title}>{C.rub(v)} ₽</span>;
  }

  function MatThumb({ coverUrl, galleryCount, size, kind }) {
    const sz = size || 44;
    if (!coverUrl) {
      return (
        <div className="cc-mat-thumb cc-mat-thumb--empty" style={{ width: sz, height: sz }} aria-hidden="true">
          <Ic n={kind === 'finish' ? 'sparkles' : 'package'} s={Math.round(sz * 0.36)} c="var(--text-tertiary)" />
        </div>
      );
    }
    return (
      <div className="cc-mat-thumb" style={{ width: sz, height: sz }}>
        <img src={coverUrl} alt="" onError={(e) => { e.target.style.display = 'none'; }} />
        {galleryCount > 1 && <span className="cc-mat-thumb-n">{galleryCount}</span>}
      </div>
    );
  }

  /* ── материалы под строкой: ПОДЧИНЁННАЯ «юбка» (паттерн конструктора) ── */
  function MatStrip({ node, est, setEst, role, status, area, flush, sub }) {
    const [pick, setPick] = useState(null); // {kind} or {replace:key,kind}
    const mats = C.nodeMaterials(est, node.id, area);
    const canRough = C.can(role, 'roughMat', status);
    const canFinish = C.can(role, 'finishMat', status);
    const canEdit = (kind) => kind === 'finish' ? canFinish : canRough;
    const procView = role === 'procurement' || role === 'super';
    const mod = (flush ? ' flush' : '') + (sub ? ' sub' : '');
    const picker = pick && (
      <MaterialPicker kind={pick.kind} onClose={() => setPick(null)}
        onPick={(matId) => { if (pick.replace) setEst(C.replaceMat(est, node.id, pick.replace, matId)); else setEst(C.addMat(est, node.id, matId)); setPick(null); }} />
    );

    // пусто → ничего (добавление материала — кнопкой на самой строке работы)
    if (!mats.length) return null;
    const total = mats.reduce((a, x) => a + x.client, 0);
    return (
      <div className={'cc-skirt' + mod}>
        <div className="cc-skirt-h">
          <span className="cc-skirt-l"><Ic n="package" s={11} />Материалы · привязано к работе</span>
          <span className="cc-skirt-div" />
          <span className="cc-skirt-n">{mats.length} поз · {C.rub(total)} ₽</span>
        </div>
        <div className="cc-skirt-list">
          {mats.map((m) => (
            <div key={m.bindingMatId} className={'cc-matrow' + (m.kind === 'finish' ? ' fin' : '')}
              title={(m.kind === 'finish' ? 'Чистовой' : 'Черновой') + ' · ' + m.name + (procView ? ' · ' + (m.sku || '') + ' · ' + (m.supplier || '') : '')}>
              <MatThumb coverUrl={m.coverUrl} galleryCount={m.galleryCount} kind={m.kind} />
              <div className="cc-mat-body">
                <div className="cc-mat-name">{m.name}</div>
                <div className="cc-mat-meta">
                  {C.can(role, 'qty', status) && canEdit(m.kind) ? (
                    <span className="cc-num" style={{ padding: '1px 6px' }}>
                      <input type="number" min={0} step={0.5} value={m.qty} onChange={(e) => setEst(C.setMatQty(est, node.id, m.bindingMatId, e.target.value))} style={{ width: 42 }} />
                      <span>{m.unit}</span>
                    </span>
                  ) : <span className="cc-mat-qty">{m.qty} {m.unit}</span>}
                  {procView && m.lead ? <span className="cc-mat-tag">lead {m.lead}д</span> : null}
                  {!m.added && <span className="cc-mat-tag" title="Привязка по умолчанию из каталога">из каталога</span>}
                  {m.added && <span className="cc-mat-tag cc-mat-tag--add">добавлено</span>}
                  {m.replaced && <span className="cc-mat-tag cc-mat-tag--rep">замена</span>}
                  {m.approval && role !== 'client' && <span className="cc-mat-flag" title="Требует согласования клиента">согл.</span>}
                </div>
              </div>
              <div className="cc-mat-price">{C.rub(m.client)} ₽</div>
              <div className="cc-mat-actions">
                {canEdit(m.kind) && <button className="cc-iconbtn" title="Заменить из библиотеки" onClick={() => setPick({ replace: m.bindingMatId, kind: m.kind })}><Ic n="replace" s={14} /></button>}
                {canEdit(m.kind) && <button className="cc-iconbtn danger" title="Убрать материал" onClick={() => setEst(C.removeMat(est, node.id, m.bindingMatId))}><Ic n="x" s={14} /></button>}
              </div>
            </div>
          ))}
        </div>
        {(canRough || canFinish) && (
          <div className="cc-skirt-add">
            {canRough && <button className="cc-addmat" onClick={() => setPick({ kind: 'rough' })}><Ic n="plus" s={12} />Черновой</button>}
            {canFinish && <button className="cc-addmat" onClick={() => setPick({ kind: 'finish' })}><Ic n="plus" s={12} />Чистовой</button>}
          </div>
        )}
        {picker}
      </div>
    );
  }

  /* ── модалка выбора материала из библиотеки (родительский источник) ── */
  function MaterialPicker({ kind, onPick, onClose }) {
    const [q, setQ] = useState('');
    const lib = C.MD.loadLibrary().filter((m) => m.isActive !== false && (!kind || m.materialKind === kind));
    const ql = q.trim().toLowerCase();
    const list = ql ? lib.filter((m) => (m.name + ' ' + (m.sku || '') + ' ' + (m.supplier || '')).toLowerCase().includes(ql)) : lib;
    return (
      <div className="cc-modal-ov" onClick={onClose}>
        <div className="cc-modal" onClick={(e) => e.stopPropagation()}>
          <div className="cc-modal-h">
            <Ic n="package-search" s={20} c="var(--primary)" />
            <h3>Библиотека материалов · {kind === 'finish' ? 'чистовые' : 'черновые'}</h3>
            <button className="cc-iconbtn" onClick={onClose}><Ic n="x" s={16} /></button>
          </div>
          <div className="cc-modal-search">
            <Ic n="search" s={16} c="var(--text-tertiary)" />
            <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск по названию, SKU, поставщику…" />
          </div>
          <div className="cc-modal-body">
            {list.length === 0 && <div className="cc-modal-empty">Ничего не найдено. Материалы добавляются в Конструкторе.</div>}
            {list.map((m) => {
              const unit = +m.clientUnitPrice || +m.unitPrice || 0;
              const cover = C.MD.matCoverUrl(m);
              const gCount = C.MD.matGallery(m).filter((g) => g.url).length;
              return (
                <button key={m.id} className="cc-pick" onClick={() => onPick(m.id)}>
                  <MatThumb coverUrl={cover} galleryCount={gCount} size={40} kind={m.materialKind} />
                  <span className="cc-pick-main">
                    <span className="cc-pick-t">{m.name}</span>
                    <span className="cc-pick-s">{m.sku ? m.sku + ' · ' : ''}{m.supplier || ''}{m.requiresClientApproval ? ' · треб. согл.' : ''}{m.leadTimeDays ? ' · lead ' + m.leadTimeDays + 'д' : ''}</span>
                  </span>
                  <span className="cc-pick-v">{C.rub(unit)} ₽/{m.unit}</span>
                </button>
              );
            })}
          </div>
        </div>
      </div>
    );
  }

  /* ── одна строка работы (задача-лист или подзадача) ── */
  function LineRow({ line, est, setEst, role, status, area, view, catalog }) {
    const hView = view || HV().loadView();
    const { node, stage, kind } = line;
    if (kind === 'sub' && HV().hiddenFor(hView, 'subtasks')) return null;
    if (kind === 'task' && HV().hiddenFor(hView, 'tasks')) return null;
    const rowOp = HV().opacityFor(hView, kind === 'sub' ? 'subtasks' : 'tasks');
    const s = C.nodeState(est, node, catalog);
    const inc = C.effectiveInc(node);
    const required = inc !== 'optional' && inc !== 'conditional';
    const depsBlock = inc === 'conditional' && s.dependsOn && s.dependsOn.length && !s.included && s.exclusionReason;
    const canInclude = C.can(role, 'include', status) && !required && !depsBlock;
    const canEditDeps = (role === 'manager' || role === 'super') && C.can(role, 'include', status) && inc === 'conditional';
    const [depPick, setDepPick] = useState(false);
    const canQty = C.can(role, 'qty', status);
    const canPrice = C.can(role, 'clientPrice', status);
    const canDecision = C.can(role, 'decision', status);
    const dec = (est.decisions || {})[node.id];
    const setInc = (on) => setEst(C.setNodeIncluded(est, catalog, node.id, on));
    const setQty = (v) => setEst({ ...est, nodes: { ...est.nodes, [node.id]: { ...(est.nodes[node.id] || {}), qty: Math.max(0, +v || 0) } } });
    const setPrice = (v) => setEst({ ...est, nodes: { ...est.nodes, [node.id]: { ...(est.nodes[node.id] || {}), price: Math.max(0, +v || 0) } } });
    const addDep = (d) => {
      const cur = s.dependsOn || [];
      if (cur.some((x) => x.nodeId === d.nodeId)) return;
      setEst(C.setDependsOnOverride(est, node.id, [...cur, d], true));
      setDepPick(false);
    };
    const removeDep = (nodeId) => {
      const cur = (s.dependsOn || []).filter((x) => x.nodeId !== nodeId);
      setEst(C.setDependsOnOverride(est, node.id, cur.length ? cur : null, true));
    };
    const decide = (state) => setEst(C.logAudit(C.setDecision(est, node.id, dec && dec.state === state ? null : state, '', 'Клиент'), 'Клиент', (state === 'approved' ? 'согласовал' : 'отклонил') + ' «' + node.name + '»'));
    const canRough = C.can(role, 'roughMat', status), canFinish = C.can(role, 'finishMat', status);
    const [addPick, setAddPick] = useState(null);
    const hasMats = C.nodeMaterials(est, node.id, area).length > 0;
    const canAddMat = s.included && !hasMats && (canRough || canFinish);

    return (
      <React.Fragment>
        <div className={'cc-line ' + (kind === 'sub' ? 'sub ' : 'task-row ') + (s.included ? '' : 'off')} style={{ opacity: rowOp }}>
          {required ? (
            <span className="cc-locked-dot" title="Обязательная работа (из каталога) — нельзя исключить"><Ic n="check" s={12} /></span>
          ) : (
            <label className="cc-check" title={depsBlock ? s.exclusionReason : (canInclude ? 'Включить / исключить' : 'Только чтение в этой роли/статусе')}>
              <input type="checkbox" checked={s.included} disabled={!canInclude} onChange={(e) => setInc(e.target.checked)} />
            </label>
          )}
          <span className="cc-line-name">
            {node.name}
            <HVis view={hView} field="executor" inline><RolePill roleId={node.role} /></HVis>
            <LevelPill level={node.level} />
            <IncPill inc={inc} cond={node.cond} dependsOn={s.dependsOn} />
            {inc === 'conditional' && <DepChips deps={s.dependsOn} customized={s.dependsOnCustomized} canEdit={canEditDeps} onRemove={removeDep} />}
            {canEditDeps && (
              <button type="button" className="cc-dep-add" onClick={() => setDepPick(true)} title="Добавить зависимость (только для этой сметы)">+ условие</button>
            )}
            {node.subcontractorOn && role !== 'client' && <span className="cc-pill sub2 ro" title="Субподряд (из каталога)">🤝 субподряд</span>}
            {node.checklistId && role !== 'client' && (
              <HVis view={hView} field="checklists" inline><span className="cc-pill cat ro" title="Чек-лист сдачи (из каталога)">📋 чек-лист</span></HVis>
            )}
          </span>

          {s.included && (
            <span className={'cc-num' + (canQty ? '' : ' ro')} title="Количество / объём">
              <input type="number" min={0} step={0.5} value={s.qty} disabled={!canQty} onChange={(e) => setQty(e.target.value)} />
              <span>{U(node.unit)}</span>
            </span>
          )}
          {s.included && (
            <HVis view={hView} field="cost" inline>
              {canPrice ? (
                <span className="cc-num price" title="Цена за единицу для клиента">
                  <input type="number" min={0} step={100} value={s.price} onChange={(e) => setPrice(e.target.value)} />
                  <span>₽</span>
                </span>
              ) : (
                <MoneyChip v={s.price} title="Цена за единицу (read-only)" />
              )}
            </HVis>
          )}
          <HVis view={hView} field="cost" inline>
            <span className="cc-line-total">{s.included ? C.rub(s.price * s.qty) + ' ₽' : '—'}</span>
          </HVis>
          {canAddMat && <button className="cc-iconbtn" title="Добавить материал из библиотеки" onClick={() => setAddPick(canRough ? 'rough' : 'finish')}><Ic n="package-plus" s={14} /></button>}

          {role === 'client' && s.included && (
            canDecision ? (
              <span className="cc-decide">
                <button className={'ok' + (dec && dec.state === 'approved' ? ' on' : '')} onClick={() => decide('approved')}><Ic n="check" s={12} />ОК</button>
                <button className={'no' + (dec && dec.state === 'rejected' ? ' on' : '')} onClick={() => decide('rejected')}><Ic n="x" s={12} /></button>
              </span>
            ) : dec ? (
              <span className={'cc-decide-badge ' + dec.state}>{dec.state === 'approved' ? '✓ согл.' : '✕ откл.'}</span>
            ) : null
          )}
          {role !== 'client' && dec && <span className={'cc-decide-badge ' + dec.state} title="Решение клиента">{dec.state === 'approved' ? '✓ клиент' : '✕ клиент'}</span>}
        </div>
        {s.included && !HV().hiddenFor(hView, 'materials') && (
          <span style={{ opacity: HV().opacityFor(hView, 'materials'), display: 'contents' }}>
            <MatStrip node={node} est={est} setEst={setEst} role={role} status={status} area={area} sub={kind === 'sub'} />
          </span>
        )}
        {addPick && <MaterialPicker kind={addPick} onClose={() => setAddPick(null)} onPick={(matId) => { setEst(C.addMat(est, node.id, matId)); setAddPick(null); }} />}
        {depPick && (
          <div className="cc-modal-ov" onClick={() => setDepPick(false)}>
            <div className="cc-modal cc-modal--dep" onClick={(e) => e.stopPropagation()}>
              <div className="cc-modal-h">
                <h3>Зависимости · только эта смета</h3>
                <button className="cc-iconbtn" onClick={() => setDepPick(false)}><Ic n="x" s={16} /></button>
              </div>
              <div className="cc-modal-body cc-modal-body--dep">
                <CalcDepPicker catalog={catalog} selfId={node.id} onAdd={addDep} excludeIds={(s.dependsOn || []).map((d) => d.nodeId)} />
              </div>
            </div>
          </div>
        )}
      </React.Fragment>
    );
  }

  /* ── manual line (вне каталога, только Менеджер/Super) ── */
  function ManualRow({ ml, est, setEst, role, status }) {
    const canEdit = C.can(role, 'manual', status);
    const up = (patch) => setEst(C.updateManualLine(est, ml.id, patch));
    return (
      <div className="cc-line manual">
        <span className="cc-locked-dot" style={{ background: 'color-mix(in srgb, var(--primary) 14%, var(--card))', color: 'var(--primary)', borderColor: 'transparent' }} title="Позиция вне каталога"><Ic n="plus" s={12} /></span>
        <span className="cc-line-name">
          {canEdit ? <input value={ml.name} onChange={(e) => up({ name: e.target.value })} /> : ml.name}
          <span className="cc-pill" style={{ background: 'color-mix(in srgb, var(--primary) 13%, var(--card))', color: 'var(--primary)' }} title={'Вне каталога · добавил ' + ml.author}>вне каталога</span>
          {ml.approvedBy ? <span className="cc-pill cat" title="Согласовано">✓ {ml.approvedBy}</span> : <span className="cc-pill inc-cond" title="Требует согласования (аудит)">ждёт согл.</span>}
        </span>
        <span className={'cc-num' + (canEdit ? '' : ' ro')}>
          <input type="number" min={0} step={0.5} value={ml.qty} disabled={!canEdit} onChange={(e) => up({ qty: Math.max(0, +e.target.value || 0) })} />
          <span>{U(ml.unit)}</span>
        </span>
        <span className={'cc-num price' + (canEdit ? '' : ' ro')}>
          <input type="number" min={0} step={100} value={ml.price} disabled={!canEdit} onChange={(e) => up({ price: Math.max(0, +e.target.value || 0) })} />
          <span>₽</span>
        </span>
        <span className="cc-line-total">{C.rub(ml.price * ml.qty)} ₽</span>
        {canEdit && <button className="cc-iconbtn danger" title="Удалить позицию" onClick={() => setEst(C.removeManualLine(est, ml.id))}><Ic n="trash-2" s={14} /></button>}
      </div>
    );
  }

  /* ── карточка этапа ── */
  function StageCard({ phase, stage, est, setEst, role, status, area, collapsed, onToggle, view, expandedTasks, onToggleTask, catalog }) {
    const v = view || HV().loadView();
    if (HV().hiddenFor(v, 'stages')) return null;
    const stageOp = HV().opacityFor(v, 'stages');
    const open = !collapsed;
    const tasks = (stage.tasks || []).filter((tk) => C.visibleTo(tk, role));
    const manual = (est.manualLines || []).filter((m) => m.stageId === stage.id);
    // экономика этапа (для клиента)
    let stageTotal = 0, anyIncluded = false;
    const countLines = (tk) => {
      if (tk.sub && tk.sub.length) tk.sub.filter((su) => C.visibleTo(su, role)).forEach((su) => { const s = C.nodeState(est, su, catalog); if (s.included) { stageTotal += s.price * s.qty; anyIncluded = true; } });
      else { const s = C.nodeState(est, tk, catalog); if (s.included) { stageTotal += s.price * s.qty; anyIncluded = true; } }
    };
    tasks.forEach(countLines);
    manual.forEach((m) => { stageTotal += m.price * m.qty; anyIncluded = true; });
    const canManual = C.can(role, 'manual', status);
    const canInclude = C.can(role, 'include', status);

    const setStageIncluded = (on) => {
      let next = { ...est, nodes: { ...est.nodes } };
      tasks.forEach((tk) => {
        if (tk.sub && tk.sub.length) tk.sub.filter((su) => C.visibleTo(su, role)).forEach((su) => {
          next = C.setNodeIncluded(next, catalog, su.id, on, { skipSync: true });
        });
        else next = C.setNodeIncluded(next, catalog, tk.id, on, { skipSync: true });
      });
      setEst(C.applyConditionalInclusion(next, catalog));
    };

    const lineCount = tasks.reduce((n, tk) => {
      if (tk.sub && tk.sub.length) return n + tk.sub.filter((su) => C.visibleTo(su, role)).length;
      return n + 1;
    }, 0);

    return (
      <div className={'cc-stage' + (anyIncluded ? '' : ' off')} style={{ opacity: stageOp }}>
        <div className="cc-stage-h" onClick={(e) => { if (e.target.closest('button,input,label,a')) return; onToggle(); }}>
          <button className={'cc-chev' + (open ? ' open' : '')} onClick={onToggle}><Ic n="chevron-right" s={16} /></button>
          <span className="cc-stage-name">{stage.name}</span>
          {stage.cond && !(stage.dependsOn && stage.dependsOn.length) && <span className="cc-pill inc-cond ro dep-badge--legacy" title="Устаревшее условие этапа">⌥ {stage.cond}</span>}
          {(stage.dependsOn || []).length > 0 && (
            <span className="cc-pill inc-cond ro" title={'Зависит от: ' + stage.dependsOn.map((d) => d.path || d.nodeId).join(', ')}>
              ⌥ {(stage.dependsOn[0].path || stage.dependsOn[0].nodeId).split(' › ').pop()}
            </span>
          )}
          <span className="cc-stage-meta">{lineCount} поз{manual.length ? ' · +' + manual.length + ' доп' : ''}</span>
          <HVis view={v} field="cost" inline><MoneyChip v={Math.round(stageTotal)} strong title="Стоимость этапа для клиента" /></HVis>
          {canInclude && (
            <span className="cc-decide" onClick={(e) => e.stopPropagation()}>
              <button onClick={() => setStageIncluded(true)} title="Включить все позиции этапа"><Ic n="check-check" s={12} /></button>
              <button onClick={() => setStageIncluded(false)} title="Исключить опциональные"><Ic n="square" s={12} /></button>
            </span>
          )}
        </div>
        {open && (
          <div className="cc-lines">
            {stage.note && role !== 'client' && (
              <HVis view={v} field="notes">
                <div className="cc-stage-note">💡 {stage.note}</div>
              </HVis>
            )}
            {lineCount === 0 && manual.length === 0 && <div style={{ fontSize: 13, color: 'var(--text-tertiary)', padding: '8px 4px' }}>Нет доступных позиций для вашей роли.</div>}
            {tasks.map((tk) => {
              const subs = (tk.sub || []).filter((su) => C.visibleTo(su, role));
              if (subs.length) {
                const tExp = expandedTasks[tk.id];
                if (HV().hiddenFor(v, 'tasks') && HV().hiddenFor(v, 'subtasks')) return null;
                return (
                  <React.Fragment key={tk.id}>
                    {!HV().hiddenFor(v, 'tasks') && (
                      <div className="cc-line task-group" style={{ opacity: HV().opacityFor(v, 'tasks') }}>
                        <button type="button" className={'cc-chev' + (tExp ? ' open' : '')} onClick={() => onToggleTask(tk.id)} title="Подзадачи"><Ic n="chevron-right" s={14} /></button>
                        <span className="cc-line-name">
                          {tk.name}
                          <HVis view={v} field="executor" inline><RolePill roleId={tk.role} /></HVis>
                          {tk.checklistId && role !== 'client' && (
                            <HVis view={v} field="checklists" inline><span className="cc-pill cat ro" title="Чек-лист сдачи (из каталога)">📋 чек-лист</span></HVis>
                          )}
                        </span>
                        <HVis view={v} field="duration" inline>{tk.days ? <span className="cc-pill ro">{tk.days} раб.дн</span> : null}</HVis>
                        <HVis view={v} field="pauses" inline>{tk.pause ? <span className="cc-pill ro">+{tk.pause} простой</span> : null}</HVis>
                        <span className="cc-line-meta">{subs.length} подзад.</span>
                      </div>
                    )}
                    {tExp && subs.map((su) => (
                      <LineRow key={su.id} line={{ node: su, stage, task: tk, kind: 'sub' }} est={est} setEst={setEst} role={role} status={status} area={area} view={v} catalog={catalog} />
                    ))}
                  </React.Fragment>
                );
              }
              return <LineRow key={tk.id} line={{ node: tk, stage, task: tk, kind: 'task' }} est={est} setEst={setEst} role={role} status={status} area={area} view={v} catalog={catalog} />;
            })}
            {manual.map((m) => <ManualRow key={m.id} ml={m} est={est} setEst={setEst} role={role} status={status} />)}
            {canManual && (
              <button className="cc-addrow" onClick={() => setEst(C.addManualLine(est, stage.id, C.ROLE_META[role].short))}>
                <Ic n="plus" s={14} />Позиция вне каталога
              </button>
            )}
          </div>
        )}
      </div>
    );
  }

  /* ── дерево целиком ── */
  function EstimateTree({ est, setEst, role, catalog }) {
    const [view, setView] = useState(() => HV().loadView());
    const [collapsed, setCollapsed] = useState({});
    const [expandedTasks, setExpandedTasks] = useState({});
    const status = est.status;
    const area = est.project.area;
    const toggle = (id) => setCollapsed((c) => ({ ...c, [id]: !c[id] }));
    const toggleTask = (id) => setExpandedTasks((c) => ({ ...c, [id]: !c[id] }));
    const Pkg = window.Calc2Pkg;
    const pk = C.packageSlots(est, catalog);
    // виртуальные карточки пакетов в позиции якоря
    const slotCards = (pi, mode, stageId) => Pkg ? C.slotsAt(pk.byPhase, pi, mode, stageId).map((slot) => (
      <Pkg.PkgStageCard key={slot.id} slot={slot} est={est} setEst={setEst} role={role} status={status}
        collapsed={!!collapsed[slot.id]} onToggle={() => toggle(slot.id)} />
    )) : null;

    useEffect(() => {
      const onStorage = (e) => { if (e.key === HV().STORAGE_KEY) setView(HV().loadView()); };
      window.addEventListener('storage', onStorage);
      return () => window.removeEventListener('storage', onStorage);
    }, []);

    useEffect(() => {
      if (!catalog.length) return;
      const patch = HV().expandToDepth(catalog, view.expandDepth);
      setCollapsed(patch.collapsed);
      setExpandedTasks(patch.expandedTasks);
    }, [view.expandDepth, catalog]);

    const applyExpand = ({ collapsed: c, expandedTasks: e }) => {
      setCollapsed(c);
      setExpandedTasks(e);
    };

    const updateView = (next) => {
      setView(next);
      HV().saveView(next);
    };

    if (!catalog.length) {
      return <div className="cc-empty"><Ic n="layers" s={26} c="var(--text-tertiary)" /><div style={{ marginTop: 10 }}>Каталог конструктора пуст. Откройте Конструктор проектов и опубликуйте каталог.</div></div>;
    }

    return (
      <div className="cc-tree-wrap">
        {window.HierarchyView && window.HierarchyViewBar && (
          <div style={{ marginBottom: 14 }}>
            <HierarchyViewBar view={view} onChange={updateView} catalog={catalog} onApplyExpand={applyExpand} showExpand={false} />
          </div>
        )}
        {window.HierarchyView && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 10 }}>
            <div className="hview-seg" role="group">
              {HV().EXPAND_OPTIONS.map((o) => (
                <button key={o.v} type="button" className={view.expandDepth === o.v ? 'on' : ''} onClick={() => updateView({ ...view, expandDepth: o.v })}>{o.l}</button>
              ))}
            </div>
          </div>
        )}
      {Pkg && <Pkg.PackageWarnings est={est} catalog={catalog} />}
      <div className="cc-tree">
        {catalog.map((ph, pi) => {
          const phOpen = !collapsed[ph.id];
          const visStages = (ph.stages || []).filter((st) => (st.tasks || []).some((tk) => C.visibleTo(tk, role) || (tk.sub || []).some((s) => C.visibleTo(s, role))));
          const hasPkg = Object.keys(pk.byPhase[pi] || {}).length > 0;
          if (!visStages.length && !hasPkg) return null;
          return (
            <div key={ph.id}>
              <div className="cc-phase-h">
                <button className="cc-phase-chev" style={{ transform: phOpen ? 'rotate(90deg)' : 'none' }} onClick={() => toggle(ph.id)}><Ic n="chevron-right" s={18} /></button>
                <span className="cc-phase-name">{ph.name}</span>
                <span className="cc-phase-meta">{visStages.length} эт.{hasPkg ? ' · 📦' : ''}</span>
              </div>
              {phOpen && (
                <div className="cc-phase-body">
                <div className="cc-stages">
                  {(ph.stages || []).map((st) => (
                    <React.Fragment key={st.id}>
                      {slotCards(pi, 'before', st.id)}
                      {visStages.includes(st) && (
                        <StageCard phase={ph} stage={st} est={est} setEst={setEst} role={role} status={status} area={area}
                          collapsed={!!collapsed[st.id]} onToggle={() => toggle(st.id)} view={view}
                          expandedTasks={expandedTasks} onToggleTask={toggleTask} catalog={catalog} />
                      )}
                      {slotCards(pi, 'after', st.id)}
                    </React.Fragment>
                  ))}
                  {slotCards(pi, 'end')}
                </div>
                </div>
              )}
            </div>
          );
        })}
      </div>
      </div>
    );
  }

  Object.assign(window, { EstimateTree, MaterialPicker, MatStrip, RolePill, MoneyChip });
})();
