/* calc2-wizard-forms.jsx — формы этапов 0–3 пошагового мастера сделки (Клиент · Объект · Ремонт и пожелания · Замер).
   Пишут в ту же карточку объекта (SiteIntake), что и остальные экраны: старые поля сохраняются (clientName,
   prefs.finishTier, rooms[].lengthM…), новые добавляются рядом (nameParts, lead, access, rooms[].openings/defects/
   engineering/base, references с imageUrl). Форма замера — по ТЗ CRM · карточка объекта · замер §5 (компактный аккордеон). */
(function () {
  const { Badge } = window.DesignSystem_9e0a09;
  const { Ic } = window;
  const { useState, useMemo, useRef, useEffect } = React;
  const R = window.CalcIntakeReadiness;
  const CD = window.ConstructorData;

  /* ─── словари ─── */
  const HOUSING = [['apartment', 'Квартира'], ['apart_hotel', 'Апартаменты'], ['house', 'Дом'], ['commercial', 'Коммерция']];
  // ось «Тип объекта» (Calc2Axes.objectFor): этажи дома и назначение коммерции → intake.floors / intake.purpose
  const FLOORS_N = [[1, '1'], [2, '2']];
  const PURPOSE = [['office', 'Офис / студия'], ['other', 'Другое']];
  const OBJ_TYPES = [['new_build', 'Новостройка'], ['secondary', 'Вторичка']];
  const FINISH = [['without', 'Без отделки'], ['partial', 'Черновая (white box)'], ['renovation', 'С ремонтом'], ['keep', 'С ремонтом, без демонтажа']];
  const ROOMS_N = [['studio', 'Студия'], ['1', '1'], ['2', '2'], ['3', '3'], ['4+', '4+']];
  const BATHS_N = [['1', '1'], ['2', '2'], ['3+', '3+']];
  const HOUSE_TYPES = [['monolith', 'Монолит'], ['panel', 'Панель'], ['brick', 'Кирпич'], ['block', 'Блочный'], ['wood', 'Дерево']];
  const OUTDOOR = ['Балкон', 'Лоджия', 'Терраса', 'Веранда', 'Патио / двор', 'Мансарда', 'Крыша / эксплуатируемая кровля', 'Нет'];
  const LEVELS = [['1', '1'], ['2', '2'], ['3+', '3+']];
  const CITIES = ['Москва', 'Московская обл.', 'Санкт-Петербург', 'Казань', 'Краснодар', 'Сочи', 'Екатеринбург', 'Новосибирск', 'Нижний Новгород'];
  const REPAIR = [['cosmetic', 'Косметический'], ['capital', 'Капитальный']];
  const TIERS = [['economy', 'Эконом'], ['mid', 'Комфорт'], ['premium', 'Премиум']];
  const YESNO = [['yes', 'Да'], ['no', 'Нет']];
  const STYLES = ['Минимализм', 'Тёплый сканди', 'Современная классика', 'Лофт', 'Япанди', 'Неоклассика', 'Пока не знаю'];
  // сложные технические решения → этапы каталога (заготовка состава работ на этапе 5); таблица и сборка — lk/calc2-blueprint.js,
  // одна для мастера и для «Глазами клиента» в Конструкторе
  const BP = window.Calc2Blueprint;
  const TECH = BP.TECH;
  const LIFT = [['cargo', 'Грузовой'], ['passenger', 'Пассажирский'], ['none', 'Нет лифта']];
  const PARKING = [['free', 'Свободная'], ['paid', 'Платная'], ['none', 'Нет / далеко']];
  const LEAD_SOURCES = [['site', 'Сайт'], ['call', 'Звонок'], ['referral', 'Рекомендация'], ['ads', 'Реклама'], ['partner', 'Партнёр'], ['repeat', 'Повторный']];
  const COMMENT_CHIPS = ['Живём в квартире во время ремонта', 'Есть животные', 'Работать только в будни', 'Нужна помощь с закупкой', 'Бюджет ограничен', 'Сроки критичны', 'Соседи чувствительны к шуму'];

  const uid = (p) => (p || 'x') + Math.random().toString(36).slice(2, 8);
  const f1 = (n) => Math.round(n * 10) / 10;
  const has = (v) => v != null && v !== '';

  /* ─── примитивы формы (в стиле cc-ir-*) ─── */
  const Req = () => <span className="cc-wf-req" title="обязательно">*</span>;
  function Fld({ label, children, hint, wide, k, req, miss }) {
    return (
      <label className={'cc-ir-field' + (wide ? ' cc-wf-wide' : '') + (req && miss ? ' is-missing' : '')} data-wz-field={k}>
        <span className="cc-ir-field-l">{label}{req && <Req />}</span>
        {children}
        {hint && <span className="cc-wf-hint">{hint}</span>}
      </label>
    );
  }
  function Txt({ value, onChange, type, placeholder, min, step, style }) {
    return <input className="cc-ir-input" type={type || 'text'} inputMode={type === 'number' ? 'decimal' : undefined} value={value == null ? '' : value} placeholder={placeholder} min={min} step={step} style={style}
      onChange={(e) => onChange(type === 'number' ? (e.target.value === '' ? '' : +e.target.value) : e.target.value)} />;
  }
  function Area({ value, onChange, rows, placeholder }) {
    return <textarea className="cc-ir-input cc-ir-textarea" rows={rows || 2} value={value || ''} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />;
  }
  function Chips({ label, options, value, onChange, hint, k, req, miss }) {
    return (
      <div className={'cc-ir-field' + (req && miss ? ' is-missing' : '')} data-wz-field={k}>
        {label && <span className="cc-ir-field-l">{label}{req && <Req />}</span>}
        <div className="cc-ir-chip-row">
          {options.map(([k, l]) => <button key={k} type="button" className={'cc-ir-chip-btn' + (value === k ? ' on' : '')} onClick={() => onChange(k)}>{l}</button>)}
        </div>
        {hint && <span className="cc-wf-hint">{hint}</span>}
      </div>
    );
  }
  function MultiChips({ label, options, value, onChange, hint, k, req, miss }) {
    const cur = value || [];
    const toggle = (x) => onChange(cur.includes(x) ? cur.filter((y) => y !== x) : [...cur, x]);
    return (
      <div className={'cc-ir-field' + (req && miss ? ' is-missing' : '')} data-wz-field={k}>
        {label && <span className="cc-ir-field-l">{label}{req && <Req />}</span>}
        <div className="cc-ir-chip-row">
          {options.map((o) => { const [k, l] = Array.isArray(o) ? o : [o, o]; return <button key={k} type="button" className={'cc-ir-chip-btn' + (cur.includes(k) ? ' on' : '')} onClick={() => toggle(k)}>{l}</button>; })}
        </div>
        {hint && <span className="cc-wf-hint">{hint}</span>}
      </div>
    );
  }
  function Counter({ label, value, onChange }) {
    const v = +value || 0;
    return (
      <div className="cc-wf-counter">
        <span>{label}</span>
        <div className="cc-wf-counter-c">
          <button type="button" onClick={() => onChange(Math.max(0, v - 1))} aria-label={'минус ' + label}>−</button>
          <b className="t-num">{v}</b>
          <button type="button" onClick={() => onChange(v + 1)} aria-label={'плюс ' + label}>+</button>
        </div>
      </div>
    );
  }
  /* карточка этапа; noteKey — «+ комментарий» в шапке: свободный текст к этому блоку (intake.notes[noteKey]) */
  function Card({ title, sub, children, action, wide, noteKey, intake, patch }) {
    const note = noteKey && intake && intake.notes ? intake.notes[noteKey] : '';
    const [open, setOpen] = useState(!!note);
    const setNote = (v) => patch && patch({ notes: { ...((intake && intake.notes) || {}), [noteKey]: v } });
    return (
      <div className={'cc-ir-card cc-wf-card' + (wide ? ' cc-wf-wide' : '')}>
        <div className="cc-wf-card-h">
          <div><div className="cc-ir-card-h">{title}</div>{sub && <div className="cc-wf-card-s">{sub}</div>}</div>
          <div className="cc-wf-card-a">{action}{noteKey && patch && <button type="button" className={'cc-wf-mini' + (note ? ' has' : '')} onClick={() => setOpen((v) => !v)} title="Комментарий к блоку"><Ic n={note ? 'message-square-text' : 'plus'} s={12} />{note ? 'комментарий' : 'комментарий'}</button>}</div>
        </div>
        {children}
        {noteKey && open && <Area rows={2} value={note} onChange={setNote} placeholder="Что важно по этому блоку — свободно" />}
      </div>
    );
  }
  /* общий комментарий этапа — снизу каждого этапа мастера (intake.stageNotes[stageId]) */
  function StageNote({ intake, patch, stageId }) {
    const v = ((intake && intake.stageNotes) || {})[stageId] || '';
    return (
      <div className="cc-ir-card cc-wf-card cc-wf-wide cc-wf-stagenote">
        <div className="cc-wf-card-h"><div><div className="cc-ir-card-h">Общий комментарий к этапу</div><div className="cc-wf-card-s">всё, что не легло в поля выше — увидят все, кто откроет сделку</div></div></div>
        <Area rows={3} value={v} onChange={(x) => patch({ stageNotes: { ...((intake && intake.stageNotes) || {}), [stageId]: x } })} placeholder="Свободный текст" />
      </div>
    );
  }

  /* загрузка файлов: Supabase Storage через window.Files, без него — blob-URL этой сессии */
  async function pickFiles(accept, multiple) {
    return new Promise((resolve) => {
      const inp = document.createElement('input'); inp.type = 'file'; inp.accept = accept || 'image/*'; inp.multiple = !!multiple;
      inp.onchange = () => resolve(Array.from(inp.files || [])); inp.click();
    });
  }
  async function uploadFiles(files, opts) {
    if (window.Files && window.Files.uploadAll) return window.Files.uploadAll(files, opts);
    return files.map((f) => ({ url: URL.createObjectURL(f), name: f.name, type: f.type, size: f.size, kind: f.type.startsWith('image/') ? 'image' : 'file', local: true }));
  }

  /* ═══════════ этап 0 · Клиент ═══════════ */
  function splitName(full) {
    const p = String(full || '').trim().split(/\s+/).filter(Boolean);
    return { last: p[0] || '', first: p[1] || '', middle: p.slice(2).join(' ') };
  }
  function ClientStage({ intake, patch, role, missing, dealId }) {
    const miss = (k) => !!(missing && missing.has(k));
    const np = intake.nameParts || splitName(intake.clientName);
    const setPart = (k, v) => { const n = { ...np, [k]: v }; patch({ nameParts: n, clientName: [n.last, n.first, n.middle].map((x) => x.trim()).filter(Boolean).join(' ') }); };
    const lead = intake.lead || null;
    const utm = lead && lead.utm ? Object.entries(lead.utm).filter(([, v]) => v) : [];
    const isSuper = role === 'super';
    return (
      <div className="cc-wf-grid">
        <Card title="Заказчик" sub="кто принимает решения по ремонту" noteKey="client" intake={intake} patch={patch}>
          <div className="cc-wf-row3">
            <Fld label="Фамилия" k="name" req miss={miss('name')}><Txt value={np.last} onChange={(v) => setPart('last', v)} placeholder="Иванова" /></Fld>
            <Fld label="Имя" req miss={miss('name')}><Txt value={np.first} onChange={(v) => setPart('first', v)} placeholder="Мария" /></Fld>
            <Fld label="Отчество"><Txt value={np.middle} onChange={(v) => setPart('middle', v)} placeholder="Сергеевна" /></Fld>
          </div>
          <div className="cc-wf-row2">
            <Fld label="Телефон" k="phone" req miss={miss('phone')}><Txt value={intake.clientPhone} onChange={(v) => patch({ clientPhone: v })} type="tel" placeholder="+7 9xx xxx-xx-xx" /></Fld>
            <Fld label="E-mail" hint="для ссылки в кабинет и сметы"><Txt value={intake.clientEmail} onChange={(v) => patch({ clientEmail: v })} type="email" placeholder="name@mail.ru" /></Fld>
          </div>
        </Card>
        <Card title="Источник заявки" sub={lead ? 'заявка пришла автоматически — поля ниже из формы сайта' : 'откуда клиент узнал о нас'} noteKey="lead" intake={intake} patch={patch}>
          <Chips options={LEAD_SOURCES} value={intake.leadSource} onChange={(v) => patch({ leadSource: v })} />
          {lead && (
            <div className="cc-wf-lead">
              <div className="cc-wf-lead-h"><Ic n="globe" s={13} />Заявка с сайта{lead.createdAt ? ' · ' + new Date(lead.createdAt).toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : ''}</div>
              {lead.page && <div className="cc-wf-lead-row"><span>страница</span><b>{lead.page}</b></div>}
              {utm.map(([k, v]) => <div key={k} className="cc-wf-lead-row"><span>utm_{k}</span><b>{v}</b></div>)}
              {lead.message && <div className="cc-wf-lead-row"><span>сообщение</span><b>{lead.message}</b></div>}
              {!utm.length && !lead.page && <div className="cc-wf-hint">Меток нет — ссылка на форму была без utm.</div>}
            </div>
          )}
          {!lead && isSuper && <div className="cc-wf-hint">Заявки с сайта приходят сюда с utm-метками автоматически: форма сайта открывает калькулятор ссылкой <code>calculator.html?new=1&amp;utm_source=…&amp;name=…&amp;phone=…</code>.</div>}
          <Fld label="Комментарий" wide><Area value={intake.clientNote} onChange={(v) => patch({ clientNote: v })} placeholder="Как удобнее связываться, что важно клиенту" /></Fld>
        </Card>
        {window.DealTeamCard && (
          <Card wide title="Команда сделки" sub="кто ведёт сделку: менеджер, замерщик (с этапа «Замер»), дизайнер, прораб; исполнители — после запуска проекта">
            <window.DealTeamCard dealId={dealId} canEdit title={false} />
          </Card>
        )}
      </div>
    );
  }

  /* ═══════════ этап 1 · Объект ═══════════ */
  // этапы техрешений анкеты (prefs.tech → TECH): мастер включает их всегда, даже если состояние объекта их выключило
  const techIncludes = (intake) => BP.techIncludes(intake);
  // прежняя зашитая логика исходного состояния — запасной вариант, пока таблицы осей (lk/calc2-template-axes.js) не загружены;
  // в обычной работе состояние берётся из таблицы «Исходное состояние» Конструктора (Calc2Axes.stateFor)
  function legacyStateBlueprint(it, out) {
    const nb = it.objectType === 'new_build';
    const fin = it.finishState;
    if (nb && fin === 'without') {
      out.exclude.push('demolition_finishes', 'demolition_partitions', 'demolition_wet_zones', 'debris_removal', 'cosmetic_prep');
      out.notes.push('Новостройка без отделки — всё с нуля: без демонтажа, стяжка, штукатурка и инженерия по чистому основанию.');
    } else if (nb && fin === 'partial') {
      out.exclude.push('demolition_finishes', 'demolition_partitions', 'demolition_wet_zones', 'cosmetic_prep');
      out.notes.push('Черновая от застройщика (white box): демонтажа нет, но проверяем стяжку и штукатурку застройщика — часть может уйти в переделку.');
    } else if (nb && fin === 'renovation') {
      out.include.push('demolition_finishes', 'debris_removal');
      out.exclude.push('demolition_partitions', 'demolition_wet_zones');
      out.notes.push('Новостройка с отделкой застройщика: обычно снимаем отделку целиком, инженерию перекладываем.');
    } else if (!nb && (fin === 'renovation' || !fin)) {
      out.include.push('demolition_finishes', 'demolition_wet_zones', 'debris_removal');
      out.exclude.push('demolition_partitions', 'cosmetic_prep');
      out.notes.push('Вторичка: состав шире и сложнее — демонтаж старой отделки и мокрых зон, вывоз мусора, замена стояков/электрики по состоянию.');
    } else if (!nb && fin === 'partial') {
      out.include.push('debris_removal');
      out.exclude.push('demolition_finishes', 'demolition_wet_zones', 'cosmetic_prep');
      out.notes.push('Вторичка после демонтажа: старой отделки уже нет — вывоз остатков и черновые работы.');
    } else if (!nb && fin === 'without') {
      out.exclude.push('demolition_finishes', 'demolition_wet_zones', 'cosmetic_prep');
      out.notes.push('Вторичка без отделки — как новостройка с нуля, но проверяем основание и старую инженерию.');
    }
  }
  // тип ремонта, который мастер применит по анкете (Calc2Assembly.presetBySlug)
  const presetFor = (intake) => BP.presetFor(intake);
  // этап входит в тип ремонта анкеты: исходное состояние не включает обратно то, что шаблон исключил (Calc2Axes.applyAxes)
  function stageInPreset(it, id) {
    const ASM = window.Calc2Assembly, C2 = window.Calc2;
    const p = ASM && ASM.presetBySlug && ASM.filterCatalogByPreset && C2 ? ASM.presetBySlug(presetFor(it)) : null;
    return !p || ASM.filterCatalogByPreset(C2.loadCatalog().catalog, p).some((x) => x.stage.id === id);
  }
  // заготовка состава работ по объекту: что войдёт / не войдёт на этапе «Состав работ».
  // est — необязательно: ручной выбор состояния в смете (est.axes.stateId) важнее анкеты.
  function objectBlueprint(intake, est) {
    const it = intake || {};
    const AX = window.Calc2Axes;
    const out = { include: [], exclude: [], notes: [], object: null };
    if (AX) {
      const ob = AX.objectFor ? AX.objectFor(it, est || null) : null; // тип объекта: дом / коммерция / квартира — применяется до состояния
      const st = AX.stateFor(it, est || null);
      if (ob) { out.object = ob; if (ob.note) out.notes.push(ob.note); }
      if (st) { out.include = (st.on || []).filter((id) => stageInPreset(it, id)); out.exclude = (st.off || []).slice(); if (st.note) out.notes.push(st.note); }
      // «войдёт» — этапы дома и коммерции; «не войдёт» от типа объекта не перечисляем (у квартиры это все 15 чужих этапов)
      if (ob) (ob.on || []).forEach((id) => { if (!out.include.includes(id) && !out.exclude.includes(id)) out.include.push(id); });
    } else legacyStateBlueprint(it, out);
    if (it.repairType === 'cosmetic') { out.notes.push('Косметический ремонт: без черновых и инженерии — подготовка поверхностей и чистовая.'); }
    // техрешения и дизайн-проект по анкете включают этапы при любом типе ремонта — как Calc2Axes.applyAxes
    techIncludes(it).concat(it.requiresDesign ? ['design_project'] : []).forEach((s) => { if (!out.include.includes(s)) out.include.push(s); out.exclude = out.exclude.filter((x) => x !== s); });
    return out;
  }
  const STAGE_NAMES = { demolition_finishes: 'демонтаж старой отделки', demolition_partitions: 'демонтаж перегородок', demolition_wet_zones: 'демонтаж мокрых зон', debris_removal: 'вывоз мусора', cosmetic_prep: 'косметическая подготовка', smart_home_wiring: 'умный дом (кабель)', heated_floor_install: 'тёплый пол', air_conditioning_rough: 'кондиционирование', ventilation_install: 'вентиляция', soundproofing: 'звукоизоляция', redevelopment_design: 'перепланировка — проект', redevelopment_approval: 'перепланировка — согласование', replanning_construction: 'возведение перегородок', window_replacement: 'окна', entrance_door: 'входная дверь' };
  // таблицу состояний правит Конструктор — этап может быть любым: название берём из каталога (с коротким кэшем), иначе id
  let _nameMap = null, _nameAt = 0;
  function nodeNames() {
    const now = Date.now();
    if (_nameMap && now - _nameAt < 3000) return _nameMap;
    const m = {};
    try { (CD ? CD.flattenCatalog(CD.loadCatalog()) : []).forEach((r) => { if (r.node && r.node.name) m[r.node.id] = r.node.name.charAt(0).toLowerCase() + r.node.name.slice(1); }); } catch (e) { /* ignore */ }
    _nameMap = m; _nameAt = now;
    return m;
  }
  const stageName = (id) => STAGE_NAMES[id] || nodeNames()[id] || id;

  function BlueprintCard({ intake }) {
    const bp = objectBlueprint(intake);
    // ось «Одна комната» выведена (KSH-502): по замеру с комнатами смета разбивается по комнатам — менеджер подтверждает «было / стало»
    const V = window.Calc2AssemblyVolumes;
    const nRooms = V && V.intakeRooms ? V.intakeRooms(intake).length : 0;
    return (
      <Card title="Заготовка для сметы" sub="что подставится на этапе «Состав работ» по типу объекта">
        {bp.object && <div className="cc-wf-bp-note"><b>Тип объекта: {bp.object.name}</b></div>}
        {bp.notes.map((n, i) => <div key={i} className="cc-wf-bp-note">{n}</div>)}
        <div className="cc-wf-bp-note"><b>Объём: весь объект</b>{nRooms > 0 ? ' — по замеру ' + nRooms + ' ' + (nRooms % 10 === 1 && nRooms % 100 !== 11 ? 'комната' : (nRooms % 10 >= 2 && nRooms % 10 <= 4 && (nRooms % 100 < 10 || nRooms % 100 >= 20)) ? 'комнаты' : 'комнат') + ': после сборки смета разобьётся по комнатам (с подтверждением «было / стало»).' : ''}</div>
        <div className="cc-wf-bp-rows">
          {bp.include.length > 0 && <div className="cc-wf-bp-row inc"><Ic n="plus-circle" s={13} /><span>войдёт: {bp.include.map(stageName).join(', ')}</span></div>}
          {bp.exclude.length > 0 && <div className="cc-wf-bp-row exc"><Ic n="minus-circle" s={13} /><span>не войдёт: {bp.exclude.map(stageName).join(', ')}</span></div>}
        </div>
        <div className="cc-wf-hint">Заготовку можно поправить вручную на этапе «Состав работ».</div>
      </Card>
    );
  }

  function ObjectStage({ intake, patch, missing }) {
    const miss = (k) => !!(missing && missing.has(k));
    const acc = intake.access || {};
    const setAcc = (k, v) => patch({ access: { ...acc, [k]: v } });
    return (
      <div className="cc-wf-grid">
        <Card title="Адрес и доступ" sub="как попасть на объект замерщику и бригаде" noteKey="access" intake={intake} patch={patch}>
          <div className={'cc-ir-field cc-wf-wide' + (miss('city') ? ' is-missing' : '')} data-wz-field="city">
            <span className="cc-ir-field-l">Город<Req /></span>
            <div className="cc-ir-chip-row">
              {CITIES.map((c) => <button key={c} type="button" className={'cc-ir-chip-btn' + (intake.city === c ? ' on' : '')} onClick={() => patch({ city: c })}>{c}</button>)}
              <button type="button" className={'cc-ir-chip-btn' + (intake.city && !CITIES.includes(intake.city) ? ' on' : '')} onClick={() => patch({ city: intake.city && !CITIES.includes(intake.city) ? intake.city : ' ' })}>Другой</button>
            </div>
            {intake.city != null && !CITIES.includes(intake.city) && <input className="cc-ir-input" style={{ marginTop: 6, maxWidth: 320 }} value={intake.city.trim()} placeholder="Название города" onChange={(e) => patch({ city: e.target.value || ' ' })} />}
          </div>
          <div className="cc-wf-row3 cc-wf-wide cc-wf-addr">
            <Fld label="Адрес" k="address" req miss={miss('address')}><Txt value={intake.address} onChange={(v) => patch({ address: v })} placeholder="Улица, дом, корпус" /></Fld>
            <Fld label={intake.housingType === 'house' ? 'Участок / дом №' : intake.housingType === 'commercial' ? 'Помещение / офис №' : 'Квартира №'} k="apt" req={intake.housingType !== 'house'} miss={miss('apt')}><Txt value={intake.apt} onChange={(v) => patch({ apt: v })} placeholder="34" /></Fld>
          </div>
          <div className="cc-wf-row3">
            <Fld label="Подъезд"><Txt value={acc.entrance} onChange={(v) => setAcc('entrance', v)} placeholder="2" /></Fld>
            <Fld label="Этаж / этажность"><Txt value={acc.floor} onChange={(v) => setAcc('floor', v)} placeholder="7 / 17" /></Fld>
            <Fld label="Код / пароль домофона"><Txt value={acc.code} onChange={(v) => setAcc('code', v)} placeholder="1234#" /></Fld>
          </div>
          <div className="cc-wf-row2">
            <Fld label="Шлагбаум / пропуск" hint="куда звонить, кому назвать номер машины"><Txt value={acc.gate} onChange={(v) => setAcc('gate', v)} placeholder="охрана +7 …, пропуск по списку" /></Fld>
            <Fld label="Контактное лицо на объекте" hint="если не заказчик: консьерж, родственник"><Txt value={acc.contact} onChange={(v) => setAcc('contact', v)} placeholder="Имя, телефон" /></Fld>
          </div>
        </Card>
        <Card title="Объект" sub="параметры, от которых зависит состав работ" noteKey="object" intake={intake} patch={patch}>
          <Chips label="Жильё" k="housing" req miss={miss('housing')} options={HOUSING} value={intake.housingType} onChange={(v) => patch({ housingType: v })} />
          {intake.housingType === 'house' && <Chips label="Этажей в доме" k="floors" options={FLOORS_N} value={Math.floor(+intake.floors) || ''} onChange={(v) => patch({ floors: v })} hint="от этого зависит, войдут ли межэтажное перекрытие и лестница; не указано — один этаж" />}
          {intake.housingType === 'commercial' && <Chips label="Назначение" k="purpose" options={PURPOSE} value={intake.purpose} onChange={(v) => patch({ purpose: v })} hint="офису добавятся перегородки, СКС, потолок и коммерческий пол; другому назначению — только общее для коммерции" />}
          <div className="cc-wf-row2">
            <Chips label="Тип объекта" k="objectType" req miss={miss('objectType')} options={OBJ_TYPES} value={intake.objectType} onChange={(v) => patch({ objectType: v })} />
            <Chips label="Отделка сейчас" k="finish" req miss={miss('finish')} options={FINISH} value={intake.finishState} onChange={(v) => patch({ finishState: v })} />
          </div>
          <div className="cc-wf-row2">
            <Chips label="Комнат" k="rooms" req miss={miss('rooms')} options={ROOMS_N} value={intake.roomsCount} onChange={(v) => patch({ roomsCount: v })} />
            <Chips label="Санузлов" options={BATHS_N} value={intake.bathsCount} onChange={(v) => patch({ bathsCount: v })} />
          </div>
          <div className="cc-wf-row2">
            <Chips label="Тип дома" options={HOUSE_TYPES} value={intake.houseType} onChange={(v) => patch({ houseType: v })} />
            <Chips label="Уровней / этажей в объекте" options={LEVELS} value={intake.levels} onChange={(v) => patch({ levels: v })} hint="двухуровневая квартира, дом в 2 этажа" />
          </div>
          {CD && CD.WALL_MATERIALS && (
            <MultiChips label="Материал стен и перегородок" options={CD.WALL_MATERIALS.map((w) => [w.id, w.label])} value={intake.wallMaterial} onChange={(v) => patch({ wallMaterial: v })} hint="для перекрёстных правил конструктора (гильзование фанеры, закладные в ГКЛ…)" />
          )}
          <MultiChips label="Открытые зоны" options={OUTDOOR} value={intake.outdoor} onChange={(v) => patch({ outdoor: v.includes('Нет') && v[v.length - 1] === 'Нет' ? ['Нет'] : v.filter((x) => x !== 'Нет') })} />
          <div className="cc-wf-row3">
            <Fld label="Площадь-ориентир, м²" hint="точная — после замера" k="area" req miss={miss('area')}><Txt type="number" min={1} step={1} value={intake.areaApprox} onChange={(v) => patch({ areaApprox: v })} placeholder="64" /></Fld>
            <Fld label="Потолки, м" hint="ориентир"><Txt type="number" min={2} step={0.05} value={intake.ceilingApprox} onChange={(v) => patch({ ceilingApprox: v })} placeholder="2.7" /></Fld>
            <Fld label="Год постройки"><Txt type="number" min={1900} step={1} value={intake.builtYear} onChange={(v) => patch({ builtYear: v })} placeholder="2021" /></Fld>
          </div>
        </Card>
        <BlueprintCard intake={intake} />
      </div>
    );
  }

  /* ═══════════ этап 2 · Ремонт и пожелания ═══════════ */
  function RepairStage({ intake, patch, dealId, missing }) {
    const miss = (k) => !!(missing && missing.has(k));
    const p = intake.prefs || {};
    const setP = (k, v) => patch({ prefs: { ...p, [k]: v } });
    const lg = p.logistics || {};
    const setLg = (k, v) => setP('logistics', { ...lg, [k]: v });
    const refs = intake.references || [];
    const [link, setLink] = useState('');
    const [busy, setBusy] = useState(false);
    const addLink = () => {
      const u = link.trim(); if (!u) return;
      let host = u; try { host = new URL(u.startsWith('http') ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch (e) {}
      patch({ references: [...refs, { id: uid('ref'), title: host, url: u.startsWith('http') ? u : 'https://' + u, source: host }] }); setLink('');
    };
    const addImages = async () => {
      const files = await pickFiles('image/*', true); if (!files.length) return;
      setBusy(true);
      try { const up = await uploadFiles(files, { pid: dealId, kind: 'reference' }); patch({ references: [...refs, ...up.map((f) => ({ id: uid('ref'), title: f.name, imageUrl: f.url, source: f.local ? 'эта сессия' : 'фото' }))] }); }
      finally { setBusy(false); }
    };
    const removeRef = (id) => patch({ references: refs.filter((r) => r.id !== id) });
    const addComment = (t) => setP('freeText', (p.freeText ? p.freeText.replace(/\s*$/, '') + '\n' : '') + t);
    const setStyle = (v) => patch({ prefs: { ...p, style: v }, brief: { ...(intake.brief || {}), style: v.filter((x) => x !== 'Пока не знаю') } });
    return (
      <div className="cc-wf-grid">
        <Card title="Ремонт" sub="базовые решения — от них зависит пресет сметы" noteKey="repair" intake={intake} patch={patch}>
          <div className="cc-wf-row3">
            <Chips label="Тип ремонта" k="repairType" req miss={miss('repairType')} options={REPAIR} value={intake.repairType} onChange={(v) => patch({ repairType: v })} />
            <Chips label="Уровень отделки" k="tier" req miss={miss('tier')} options={TIERS} value={p.finishTier} onChange={(v) => setP('finishTier', v)} />
            <Chips label="Дизайн-проект" k="dp" req miss={miss('dp')} options={YESNO} value={intake.requiresDesign == null ? undefined : (intake.requiresDesign ? 'yes' : 'no')} onChange={(v) => patch({ requiresDesign: v === 'yes', dpDecided: true })} hint="с ДП — этап 4 обязателен" />
          </div>
          <MultiChips label="Стиль" options={STYLES} value={p.style} onChange={setStyle} />
          <MultiChips label="Сложные технические решения" options={TECH.map((t) => [t[0], t[1]])} value={p.tech} onChange={(v) => setP('tech', v)} hint="сразу добавятся в состав работ" />
        </Card>
        <Card title="Референсы" sub="ссылки или картинки: Pinterest, скрины, мудборд" noteKey="refs" intake={intake} patch={patch} action={<button type="button" className="cc-ir-btn ghost" disabled={busy} onClick={addImages}><Ic n="image-plus" s={14} />{busy ? 'Загружаю…' : 'Картинки'}</button>}>
          <div className="cc-wf-linkrow">
            <input className="cc-ir-input" value={link} placeholder="https://pinterest.com/…" onChange={(e) => setLink(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addLink(); } }} />
            <button type="button" className="cc-ir-btn" onClick={addLink}><Ic n="link" s={14} />Добавить</button>
          </div>
          {refs.length > 0 && (
            <div className="cc-wf-refs">
              {refs.map((r) => (
                <div key={r.id} className="cc-wf-ref" style={r.imageUrl ? { backgroundImage: 'url(' + r.imageUrl + ')' } : undefined}>
                  {!r.imageUrl && <a href={r.url} target="_blank" rel="noreferrer" className="cc-wf-ref-link"><Ic n="external-link" s={13} />{r.title}</a>}
                  {r.imageUrl && <span className="cc-wf-ref-cap">{r.title}</span>}
                  <button type="button" className="cc-wf-ref-x" onClick={() => removeRef(r.id)} aria-label="Убрать"><Ic n="x" s={12} /></button>
                </div>
              ))}
            </div>
          )}
        </Card>
        <Card title="Сроки и логистика" noteKey="logistics" intake={intake} patch={patch}>
          <div className="cc-wf-row2">
            <Fld label="Желаемый старт"><Txt type="date" value={p.startWanted} onChange={(v) => { setP('startWanted', v); }} /></Fld>
            <Fld label="Нужно закончить к"><Txt type="date" value={p.deadlineDate} onChange={(v) => patch({ prefs: { ...p, deadlineDate: v, deadlineNote: v ? 'к ' + v : '' } })} /></Fld>
          </div>
          <div className="cc-wf-row2">
            <Chips label="Лифт" options={LIFT} value={lg.lift} onChange={(v) => setLg('lift', v)} />
            <Chips label="Парковка / разгрузка" options={PARKING} value={lg.parking} onChange={(v) => setLg('parking', v)} />
          </div>
          <div className="cc-wf-row2">
            <Chips label="Пропуск на объект" options={YESNO} value={lg.pass} onChange={(v) => setLg('pass', v)} />
            <Chips label="Хранить материалы на объекте" options={YESNO} value={lg.storage} onChange={(v) => setLg('storage', v)} />
          </div>
        </Card>
        <Card title="Комментарий" sub="нажмите подходящее — добавится в текст">
          <MultiChips options={COMMENT_CHIPS} value={COMMENT_CHIPS.filter((c) => (p.freeText || '').includes(c))} onChange={(v) => { const keep = (p.freeText || '').split('\n').filter((l) => !COMMENT_CHIPS.includes(l.trim())); setP('freeText', [...v, ...keep].join('\n').trim()); }} />
          <Area rows={3} value={p.freeText} onChange={(v) => setP('freeText', v)} placeholder="Что важно / чего не хотим" />
        </Card>
      </div>
    );
  }

  /* ═══════════ этап 3 · Замер ═══════════
     Форма замера — мастер из Конструктора (Замеры → Шаблоны → Геометрия): помещения (прямоугольник / Г-образное /
     свободный контур), стены, проёмы, конструктив, инженерия, фото, итог. Встроен через MeasurementsWizard в режиме
     сделки; сессия общая (Sync), помещения и площади пишутся в карточку объекта через MeasureDealBridge. */
  const RL = { foreman: 'Прораб', executor: 'Исполнитель', measurer: 'Замерщик', designer: 'Дизайнер', manager: 'Менеджер' };
  function teamOptions() {
    const out = [];
    try {
      const A = window.AccessStore;
      if (A && A.load) Object.values(A.load().accounts || {}).filter((a) => !a.guest && a.name && a.status !== 'revoked' && a.role !== 'client')
        .sort((a, b) => (a.trade === 'measurer' ? -1 : 1) - (b.trade === 'measurer' ? -1 : 1))
        .forEach((a) => out.push([a.name, a.name + ' · ' + (a.trade === 'measurer' ? 'Замерщик' : (a.roleLabel || RL[a.role] || a.role))]));
      const fm = JSON.parse(localStorage.getItem('remontpro_foremen') || '[]');
      fm.forEach((f) => { const nm = f.name && f.name !== '—' ? f.name : f.brigade; if (nm && !out.some((o) => o[0] === nm)) out.push([nm, nm + ' · бригада']); });
    } catch (e) {}
    return out;
  }
  function measureLink(dealId) {
    try { const u = new URL('measure.html', window.location.href); u.searchParams.set('deal', dealId); return u.toString(); } catch (e) { return 'measure.html?deal=' + dealId; }
  }
  /* профиль замерщика: аккаунт в команде (AccessStore) с ролью «Замерщик» — появляется в списке и получает ссылку на замер */
  function NewMeasurer({ onCreated }) {
    const [open, setOpen] = useState(false);
    const [name, setName] = useState('');
    const [phone, setPhone] = useState('');
    const create = () => {
      const nm = name.trim(); if (!nm) return;
      const A = window.AccessStore; if (!A || !A.upsertAccount) return;
      const id = 'meas-' + Math.random().toString(36).slice(2, 8);
      A.upsertAccount(id, { name: nm, phone: phone.trim(), role: 'executor', roleLabel: 'Замерщик', trade: 'measurer', status: 'active', zones: {}, caps: {}, invitedAt: new Date().toISOString() }, 'measurer_created');
      onCreated(nm); setOpen(false); setName(''); setPhone('');
    };
    if (!open) return <button type="button" className="cc-wf-mini" onClick={() => setOpen(true)}><Ic n="user-round-plus" s={12} />новый замерщик</button>;
    return (
      <div className="cc-wf-newmeas">
        <input className="cc-ir-input" value={name} placeholder="ФИО замерщика" onChange={(e) => setName(e.target.value)} />
        <input className="cc-ir-input" value={phone} placeholder="+7 …" onChange={(e) => setPhone(e.target.value)} />
        <button type="button" className="cc-ir-btn" onClick={create}><Ic n="check" s={14} />Создать</button>
        <button type="button" className="cc-ir-btn ghost" onClick={() => setOpen(false)}>Отмена</button>
      </div>
    );
  }
  function MeasureStage({ intake, patch, dealId, missing }) {
    const miss = (k) => !!(missing && missing.has(k));
    const [teamTick, setTeamTick] = useState(0);
    const team = useMemo(teamOptions, [teamTick]);
    const known = new Set(team.map((t) => t[0]));
    const [other, setOther] = useState(() => !!intake.measuredBy && !known.has(intake.measuredBy));
    const mbSel = other ? '__other' : (intake.measuredBy || '');
    const [copied, setCopied] = useState(false);
    const [busy, setBusy] = useState(false);
    const ms = intake.measureSession || {};
    const link = measureLink(dealId);
    const copy = () => { try { const pr = navigator.clipboard.writeText(link); if (pr && pr.catch) pr.catch(() => {}); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1800); };
    const dealProps = useMemo(() => (window.MeasureDealBridge ? window.MeasureDealBridge.dealProps(dealId) : null), [dealId, intake.measuredBy, intake.measurementDate]);
    /* перекрёстная задача замерщику: появляется в кабинете исполнителя (IntakeTasksPanel · measurer), закрывается при отправке замера */
    useEffect(() => {
      const F = window.FormInstances; if (!F || !F.upsertTask) return;
      if (!intake.measuredBy || !intake.measurementDate) return;
      if (ms.status === 'submitted') return;
      const cur = F.findTaskByKey(dealId, 'measure');
      if (cur && cur.status === 'open' && cur.assignee === intake.measuredBy && cur.sub === intake.measurementDate) return;
      const t = setTimeout(() => F.upsertTask(dealId, 'measure', { kind: 'measure', assigneeRole: 'measurer', assignee: intake.measuredBy, title: 'Выезд на замер · ' + [intake.address, intake.apt ? 'кв. ' + intake.apt : ''].filter(Boolean).join(', '), sub: intake.measurementDate, url: link, sameTab: true, actionLabel: 'Открыть замер', dueAt: intake.measurementDate }), 400);
      return () => clearTimeout(t);
    }, [dealId, intake.measuredBy, intake.measurementDate, ms.status]);
    const addPlan = async () => {
      const files = await pickFiles('image/*,application/pdf', true); if (!files.length) return;
      setBusy(true);
      try { const up = await uploadFiles(files, { pid: dealId, kind: 'plan' }); patch({ plans: [...(intake.plans || []), ...up.map((f) => ({ id: uid('pl'), name: f.name, url: f.url, kind: f.kind }))] }); }
      finally { setBusy(false); }
    };
    return (
      <div className="cc-wf-grid">
        <Card wide title="Выезд" sub="кто и когда мерил; ссылку можно отдать замерщику — он заполнит с телефона, данные общие" noteKey="measure" intake={intake} patch={patch}
          action={<span className="cc-wf-status">{ms.status === 'submitted' ? <Badge tone="success" dot>замер отправлен</Badge> : ms.rooms ? <Badge tone="info" dot>{ms.rooms} помещ. · черновик</Badge> : <Badge tone="neutral" dot>не начат</Badge>}</span>}>
          <div className="cc-wf-row3">
            <Fld label="Дата замера" k="measureDate" req miss={miss('measureDate')}><Txt type="date" value={intake.measurementDate} onChange={(v) => patch({ measurementDate: v })} /></Fld>
            <Fld label="Замерщик" k="measurer" req miss={miss('measurer')} hint={team.length ? 'из команды' : 'в команде пока никого — создайте профиль замерщика'}>
              <select className="cc-ir-input" value={mbSel} onChange={(e) => { const v = e.target.value; if (v === '__other') { setOther(true); patch({ measuredBy: '' }); } else { setOther(false); patch({ measuredBy: v }); } }}>
                <option value="">— выбрать —</option>
                {team.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
                <option value="__other">Другой…</option>
              </select>
              {other && <input className="cc-ir-input" style={{ marginTop: 6 }} value={intake.measuredBy || ''} placeholder="ФИО замерщика" onChange={(e) => patch({ measuredBy: e.target.value })} />}
              <div style={{ marginTop: 6 }}><NewMeasurer onCreated={(nm) => { setTeamTick((t) => t + 1); setOther(false); patch({ measuredBy: nm }); }} /></div>
            </Fld>
            <Fld label="Ссылка замерщику" hint="откроет этот же замер на телефоне">
              <div className="cc-wf-linkrow"><input className="cc-ir-input" readOnly value={link} onFocus={(e) => e.target.select()} /><button type="button" className="cc-ir-btn" onClick={copy}><Ic n={copied ? 'check' : 'link-2'} s={14} />{copied ? 'Скопирована' : 'Копировать'}</button></div>
            </Fld>
          </div>
        </Card>
        <div className={'cc-wf-wide cc-wf-measure' + (miss('measure') && !(intake.rooms || []).length ? ' is-missing' : '')} data-wz-field="measure">
          {window.MeasurementsWizard && dealProps
            ? <window.MeasurementsWizard deal={dealProps} />
            : <div className="cc-wf-hint">Мастер замера не загружен (measurements/measurements-app.jsx).</div>}
        </div>
        <Card wide title="План" sub="скетч от руки, план БТИ или застройщика" action={<button type="button" className="cc-ir-btn ghost" disabled={busy} onClick={addPlan}><Ic n="file-plus" s={14} />Загрузить</button>}>
          {(intake.plans || []).length === 0 ? <div className="cc-wf-hint">Пока без плана — можно добавить позже.</div>
            : <div className="cc-wf-plans">{(intake.plans || []).map((pl) => <a key={pl.id} className="chip" href={pl.url} target="_blank" rel="noreferrer"><Ic n={pl.kind === 'pdf' ? 'file-text' : 'image'} s={13} />{pl.name}</a>)}</div>}
        </Card>
      </div>
    );
  }

  /* ═══════════ этап 4 · Дизайн-проект ═══════════
     исходные данные с этапов «Объект», «Ремонт и пожелания», «Замер» + подробный бриф-анкета из Конструктора («Брифы ДП»)
     в поп-апе: дизайнер проходит её онлайн вместе с клиентом или отправляет клиенту уникальную ссылку lk/brief.html?t=…;
     «Отправить бриф» → «Согласовать» (ссылка клиента закрывается) → сводка ответов здесь и дальше — состав работ. */
  const TIER_LABEL = { economy: 'Эконом', mid: 'Комфорт', premium: 'Премиум' };
  const TECH_LEGACY = { heated_floor: 'Тёплый пол' };
  function DpStage({ intake, patch, dealId, missing, goTo, role }) {
    const miss = (k) => !!(missing && missing.has(k));
    const p = intake.prefs || {};
    const refs = intake.references || [];
    const [modal, setModal] = useState(false);
    const [copied, setCopied] = useState(false);
    const [tab, setTab] = useState(null); // вкладка блока данных; по умолчанию — бриф, пока он не отправлен, потом ответы
    const DB = window.DpBrief; const FIn = window.FormInstances;
    const [inst, setInst] = useState(() => (DB && intake.requiresDesign ? DB.ensure(dealId, intake) : null));
    useEffect(() => { if (DB && intake.requiresDesign && !inst) setInst(DB.ensure(dealId, intake)); }, [intake.requiresDesign]);
    useEffect(() => {
      const reload = () => { if (FIn) setInst(FIn.findInstance(dealId, (i) => i.kind === 'dp_brief') || null); };
      window.addEventListener('remontpro:forms-updated', reload); return () => window.removeEventListener('remontpro:forms-updated', reload);
    }, [dealId]);
    const state = inst ? inst.accessState : 'pending';
    const submitted = state === 'submitted'; const approved = state === 'approved'; const closed = submitted || approved;
    const schema = DB && inst ? DB.schemaFor(inst, intake) : null;
    const sections = (schema && schema.sections) || [];
    const stats = DB && DB.sectionStats ? sections.map((s) => DB.sectionStats(s, inst.responses || {}, intake)) : [];
    const total = stats.reduce((a, s) => a + s.total, 0); const filled = stats.reduce((a, s) => a + s.filled, 0);
    const progress = total ? Math.round((filled / total) * 100) : 0;
    const doneBlocks = stats.filter((s) => s.filled > 0).length;
    const link = DB && inst ? DB.briefLink(dealId, inst) : '';
    const linkIssued = !!inst && state !== 'pending';
    const refresh = (i) => setInst(i || (FIn ? FIn.findInstance(dealId, (x) => x.kind === 'dp_brief') : null));
    /* перекрёстные задачи: дизайнеру (кабинет дизайнера), клиенту (ссылка) — по состоянию брифа */
    useEffect(() => { if (DB && DB.syncTasks && inst) DB.syncTasks(dealId, inst, intake); }, [inst && inst.id, inst && inst.accessState, inst && inst.linkToken]);
    const copyLink = () => {
      if (FIn && inst && inst.accessState === 'pending') { FIn.updateInstance(dealId, inst.id, { accessState: 'active', linkIssuedAt: new Date().toISOString() }); refresh(); }
      try { const pr = navigator.clipboard.writeText(link); if (pr && pr.catch) pr.catch(() => {}); } catch (e) {}
      setCopied(true); setTimeout(() => setCopied(false), 1800);
    };
    const newLink = () => { if (FIn && inst && FIn.regenerateLink) refresh(FIn.regenerateLink(dealId, inst.id)); };
    const approve = () => { if (FIn && inst && FIn.approveDpBrief) { const fresh = FIn.approveDpBrief(dealId, inst.id); refresh(fresh); DB.markIntake(dealId, 'approved', 100); if (DB.syncTasks) DB.syncTasks(dealId, fresh, intake); } };
    const reopen = () => { if (FIn && inst && FIn.reopenDpBrief) { const fresh = FIn.reopenDpBrief(dealId, inst.id); refresh(fresh); DB.markIntake(dealId, 'in_progress', progress); if (DB.syncTasks) DB.syncTasks(dealId, fresh, intake); } };
    if (!intake.requiresDesign) {
      return (
        <div className="cc-wf-grid">
          <Card wide title="Дизайн-проект не заказан" sub="на этапе «Ремонт и пожелания» выбрано «Дизайн-проект: Нет» — этап пропускается">
            <div className="cc-wf-hint">Передумали — вернитесь на этап «Ремонт» и переключите «Дизайн-проект: Да». {goTo && <button type="button" className="cc-wf-mini" onClick={() => goTo(2)}>к этапу «Ремонт»</button>}</div>
          </Card>
        </div>
      );
    }
    const [stLabel, stTone] = (DB && DB.STATUS && DB.STATUS[state]) || [state, 'neutral'];
    const step = approved ? 3 : submitted ? 2 : (state === 'in_progress' || state === 'active') ? 1 : 0;
    const STEPS = ['Заполнить с клиентом или по ссылке', 'Отправить бриф', 'Согласовать', 'Передать дизайнеру'];
    return (
      <div className="cc-wf-grid">
        {window.DpProcessCard && (
          <Card wide title="Процесс дизайн-проекта" sub="бриф → разработка ДП (этапы дизайнера) → финальный ДП: презентация и техническая часть → состав работ и смета по его результатам">
            <div data-wz-field="dpFinal">
              <window.DpProcessCard dealId={dealId} intake={intake} title={false} who={role === 'designer' ? 'designer' : undefined} onBrief={() => { setTab('brief'); setModal(true); }} onWorks={goTo ? () => goTo(5) : null} />
            </div>
          </Card>
        )}
        <div className={'cc-ir-card cc-wf-card cc-wf-wide cc-wf-brief' + (miss('dpBrief') ? ' is-missing' : '') + (approved ? ' is-ok' : '')} data-wz-field="dpBrief">
          <div className="cc-wf-card-h">
            <div><div className="cc-ir-card-h">Бриф и исходные данные<Req /></div><div className="cc-wf-card-s">всё, с чем дизайнер идёт в работу: анкета клиента, его ответы и данные с этапов «Объект», «Ремонт», «Замер»</div></div>
            <Badge tone={stTone} dot>{'Бриф: ' + stLabel}</Badge>
          </div>
          {window.DealDataBlock && <window.DealDataBlock dealId={dealId} intake={intake} tab={tab} onTab={setTab} onOpenBrief={() => setModal(true)}
            sourceTools={goTo && <React.Fragment><button type="button" className="chip" onClick={() => goTo(2)}><Ic n="pencil" s={12} />этап «Ремонт»</button><button type="button" className="chip" onClick={() => goTo(3)}><Ic n="ruler" s={12} />этап «Замер»</button></React.Fragment>}
            lead={{ id: 'brief', label: 'Бриф на ДП', badge: approved ? '✓' : inst ? progress + '%' : null, render: () => (
            <React.Fragment>
              <ol className="cc-wf-steps">
                {STEPS.map((t, i) => <li key={t} className={i < step ? 'is-done' : i === step ? 'is-cur' : ''}><span className="cc-wf-steps-n">{i < step ? <Ic n="check" s={11} /> : i + 1}</span>{t}</li>)}
              </ol>
              {inst && (
                <div className="dpb-prog cc-wf-brief-prog" title={filled + ' из ' + total + ' ответов'}>
                  <div className="dpb-prog-bar"><i style={{ width: progress + '%' }} /></div>
                  <span className="t-num">{doneBlocks}/{sections.length} блоков · {progress}%</span>
                </div>
              )}
              <div className="cc-wf-brief-actions">
                <button type="button" className={'cc-ir-btn' + (closed ? ' ghost' : ' accent')} onClick={() => setModal(true)}><Ic n={closed ? 'file-text' : 'users'} s={14} />{closed ? 'Открыть бриф' : (state === 'pending' ? 'Заполнить с клиентом' : 'Продолжить с клиентом')}</button>
                {submitted && <button type="button" className="cc-ir-btn accent" onClick={approve}><Ic n="check" s={14} />Согласовать бриф</button>}
                {!closed && <button type="button" className="cc-ir-btn ghost" onClick={copyLink}><Ic n={copied ? 'check' : 'link-2'} s={14} />{copied ? 'Ссылка скопирована' : (linkIssued ? 'Скопировать ссылку' : 'Ссылка клиенту')}</button>}
                {closed && <button type="button" className="cc-ir-btn ghost" onClick={reopen}><Ic n="rotate-ccw" s={14} />Переоткрыть</button>}
              </div>
              {linkIssued && (
                <div className={'cc-wf-brief-link' + (approved ? ' is-closed' : '')}>
                  <Ic n={approved ? 'lock' : 'link'} s={13} />
                  <span className="cc-wf-brief-url">{link}</span>
                  <span className="cc-wf-brief-lstate">{approved ? 'закрыта — бриф согласован' : submitted ? 'только просмотр' : 'открыта для клиента'}</span>
                  {!approved && <button type="button" className="cc-wf-mini" onClick={newLink} title="Старая ссылка перестанет работать"><Ic n="refresh-cw" s={11} />новая ссылка</button>}
                </div>
              )}
              <div className="cc-wf-hint">
                {approved ? 'Бриф согласован ' + (inst.approvedAt ? new Date(inst.approvedAt).toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '') + ' — дизайнер ведёт этапы (процесс выше). «Работы» откроются, когда утверждены чертежи и спецификация. Правки брифа — «Переоткрыть».'
                  : submitted ? 'Ответы получены ' + (inst.submittedAt ? new Date(inst.submittedAt).toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '') + '. Проверьте вкладку «Ответы брифа» и нажмите «Согласовать» — ссылка клиента закроется, дизайнер начнёт разработку.'
                  : 'Анкета открывается в отдельном окне. Стиль, техрешения, референсы и помещения по замеру уже подставлены. По уникальной ссылке клиент заполняет сам — ответы общие; ссылка закрывается после согласования.'}
              </div>
            </React.Fragment>) }} />}
        </div>
        {modal && inst && window.DpBriefModal && (
          <window.DpBriefModal dealId={dealId} intake={intake} inst={inst} onInstance={(i) => refresh(i)} who="designer" onClose={() => { setModal(false); refresh(); }} />
        )}
      </div>
    );
  }

  /* ═══════════ этапы «Состав работ» / «Материалы» · по результатам ДП ═══════════
     комбинированный вид: смета из каталога (работы + черновые/чистовые материалы) + спецификация ДП (чистовые материалы и мебель
     с артикулами) + чертежи как основание состава работ. Спецификация не дублируется в каталожных позициях — показана рядом. */
  function DpWorksBridge({ intake, dealId, total, tab, est, setEst, catalog }) {
    const D = window.DpPipeline; const SB = window.Calc2SpecBridge;
    const [, force] = useState(0);
    useEffect(() => { const up = () => force((x) => x + 1); ['obitel:dp-pipeline', 'storage', 'obitel:sync'].forEach((e) => window.addEventListener(e, up)); return () => ['obitel:dp-pipeline', 'storage', 'obitel:sync'].forEach((e) => window.removeEventListener(e, up)); }, []);
    // pr/a вычисляются безусловно (до хуков ниже) — хуки не должны зависеть от условных ранних return
    const pr = (D && D.process && intake.requiresDesign) ? D.process(dealId, intake) : null;
    const worksReady = !!(pr && pr.works.ready);
    const a = worksReady ? (D.album(dealId) || { stages: [], spec: [], specTotal: 0 }) : { stages: [], spec: [], specTotal: 0 };
    const specPlan = useMemo(() => (SB && est && catalog ? SB.plan(est, catalog, a.spec) : []), [SB, est, catalog, a.spec]);
    const needsUpdate = specPlan.some((x) => x.status === 'new' || x.status === 'update' || x.status === 'removed');

    // перенос спецификации ДП в смету сам при первом входе, когда работы (чертежи+спека) утверждены и
    // переноса ещё не было (est.specSync); дальше — только кнопкой «Обновить из ДП» ниже.
    // ждём, пока в составе появятся работы (стартовый пресет «заменить» сбросил бы узлы спецификации); от свежего состояния
    const composed = !!est && Object.values(est.nodes || {}).some((n) => n && n.included);
    useEffect(() => {
      if (!worksReady || !SB || !setEst || !est || est.specSync || !a.spec.length || !composed) return;
      setEst((cur) => (cur.specSync ? cur : SB.apply(cur, catalog, a.spec)));
    }, [worksReady, a.spec.length, composed]);

    const transferNow = () => { if (SB && setEst && est) setEst((cur) => SB.apply(cur, catalog, a.spec)); };

    if (!D || !D.process || !intake.requiresDesign) return null;
    if (!worksReady) return null;
    const docs = a.stages.find((s) => s.id === 'docs'); const docItems = docs ? docs.items || [] : [];
    const byCat = {}; a.spec.forEach((r) => { const k = r.category || 'Прочее'; byCat[k] = byCat[k] || { n: 0, sum: 0 }; byCat[k].n += 1; byCat[k].sum += (+r.qty || 0) * (+r.price || 0); });
    const cats = Object.keys(byCat);
    const rub = (n) => Math.round(n || 0).toLocaleString('ru-RU') + ' ₽';
    const album = (t) => 'dp-album.html?deal=' + encodeURIComponent(dealId) + '&tab=' + t;

    // строки перенесённой спецификации по фазам закупки — только на вкладке «Материалы» (детальный разбор)
    const byPhase = {};
    if (tab === 'mat') {
      specPlan.forEach((row) => {
        const pi = row.phase;
        const bucket = byPhase[pi] = byPhase[pi] || { name: (catalog[pi] || {}).name || ('Фаза ' + pi), rows: [] };
        bucket.rows.push(row);
      });
    }
    const phaseIdxs = Object.keys(byPhase).map(Number).sort((x, y) => x - y);
    const STATUS_LABEL = { new: 'новая', update: 'обновлена', same: 'перенесена', removed: 'снята в ДП' };

    return (
      <div className="cc-ir-card cc-wf-card cc-wf-dpw">
        <div className="cc-wf-card-h">
          <div><div className="cc-ir-card-h">По дизайн-проекту</div><div className="cc-wf-card-s">{tab === 'mat' ? 'спецификация ДП перенесена в смету чистовыми материалами по фазам закупки ниже; в каталожных позициях выше их не дублируем' : 'чертежи — основание состава работ; спецификация — чистовые материалы и мебель'}</div></div>
          <span style={{ display: 'inline-flex', gap: 6, flexWrap: 'wrap' }}>
            {tab === 'mat' && needsUpdate && SB && <button type="button" className="cc-wf-mini" onClick={transferNow}><Ic n="refresh-cw" s={12} />Обновить из ДП</button>}
            <a className="cc-wf-mini" href={album('tech')} target="_blank" rel="noreferrer"><Ic n="file-text" s={12} />техчасть</a><a className="cc-wf-mini" href={album('pres')} target="_blank" rel="noreferrer"><Ic n="presentation" s={12} />презентация</a>
          </span>
        </div>
        <div className="cc-wf-dpw-grid">
          <div className="cc-wf-dpw-col">
            <div className="cc-wf-dpw-t"><Ic n="list" s={14} />Спецификация ДП · {a.spec.length} поз.</div>
            {cats.length ? <div className="cc-wf-kv">{cats.map((c) => <div key={c} className="cc-wf-kv-row"><span>{c} · {byCat[c].n}</span><b className="t-num">{rub(byCat[c].sum)}</b></div>)}</div> : <div className="cc-wf-hint">Спецификация пуста.</div>}
          </div>
          <div className="cc-wf-dpw-col">
            <div className="cc-wf-dpw-t"><Ic n="file-text" s={14} />Чертежи · {docItems.length}</div>
            {docItems.length ? <div className="cc-wf-dpw-files">{docItems.slice(0, 8).map((x) => <a key={x.id} href={x.url} target="_blank" rel="noreferrer"><Ic n={x.kind === 'pdf' ? 'file-text' : x.kind === 'image' ? 'image' : 'link'} s={12} />{x.name}</a>)}{docItems.length > 8 && <span className="cc-wf-hint">+{docItems.length - 8} в техчасти</span>}</div> : <div className="cc-wf-hint">Рабочая документация ещё не утверждена.</div>}
          </div>
        </div>
        {tab === 'mat' && phaseIdxs.length > 0 && (
          <div className="cc-wf-dpw-grid" style={{ marginTop: 6 }}>
            {phaseIdxs.map((pi) => (
              <div className="cc-wf-dpw-col" key={pi}>
                <div className="cc-wf-dpw-t">{byPhase[pi].name}</div>
                <div className="cc-wf-kv">
                  {byPhase[pi].rows.map((row) => (
                    <div key={row.row.id} className="cc-wf-kv-row">
                      <span>{row.category.label} · {row.row.name}{row.row.qty ? ' · ' + row.row.qty + ' ' + (row.row.unit || 'шт') : ''}</span>
                      <b className="t-num">{rub((+row.row.qty || 0) * (+row.row.price || 0))} · {STATUS_LABEL[row.status] || row.status}</b>
                    </div>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
        <div className="cc-wf-dpw-total">
          <span>Смета по каталогу (работы + материалы)</span><b className="t-num">{rub(total)}</b>
          <span>Спецификация ДП (чистовые материалы, мебель)</span><b className="t-num">{rub(a.specTotal)}</b>
          <span className="is-sum">Итого клиенту с ДП</span><b className="t-num is-sum">{rub((total || 0) + (a.specTotal || 0))}</b>
        </div>
      </div>
    );
  }

  /* ═══════════ этап 4 · Инженерия по дизайн-проекту ═══════════
     помещения, где замерщик отметил «уточнить по ДП»: итоговые точки (розетки, выключатели, вода, канализация…) вносятся
     здесь и пишутся в ту же сессию замера → карточку объекта → смету */
  const ENG_KINDS = [['socket', 'Розетки'], ['switch', 'Выключатели'], ['water_outlet', 'Выводы воды'], ['sewer_point', 'Канализация'], ['radiator', 'Радиаторы'], ['low_voltage', 'Слаботочка'], ['ac_line', 'Кондиционер']];
  function DpEngineeringStage({ intake, dealId }) {
    const B = window.MeasureDealBridge;
    const key = B ? B.storageKey(dealId) : null;
    const [session, setSession] = useState(() => { try { return key ? JSON.parse(localStorage.getItem(key)) : null; } catch (e) { return null; } });
    if (!session) return null;
    const rooms = (session.object.rooms || []).filter((r) => r.engineeringByDp);
    if (!rooms.length) return null;
    const save = (next) => { try { localStorage.setItem(key, JSON.stringify(next)); } catch (e) {} setSession(next); if (B) B.applyToIntake(dealId, next); };
    const setCount = (rid, kind, count) => {
      const next = { ...session, object: { ...session.object, rooms: session.object.rooms.map((r) => {
        if (r.id !== rid) return r;
        const rest = (r.engineeringPoints || []).filter((x) => x.kind !== kind);
        return { ...r, engineeringPoints: count > 0 ? rest.concat([{ kind, count, source: 'dp' }]) : rest, engineeringDpAt: new Date().toISOString() };
      }) } };
      save(next);
    };
    const done = (rid, v) => save({ ...session, object: { ...session.object, rooms: session.object.rooms.map((r) => (r.id === rid ? { ...r, engineeringDpDone: v } : r)) } });
    return (
      <Card wide title="Инженерия по дизайн-проекту" sub={rooms.length + ' помещ. — замерщик отметил «уточнить по ДП»: внесите итоговые точки из проекта, они уйдут в карточку объекта и смету'}>
        <div className="cc-wf-rooms">
          {rooms.map((r) => {
            const get = (k) => { const p = (r.engineeringPoints || []).find((x) => x.kind === k); return p ? +p.count || 0 : 0; };
            return (
              <div key={r.id} className={'cc-wf-room is-open' + (r.engineeringDpDone ? ' is-ok' : '')}>
                <div className="cc-wf-room-h" style={{ cursor: 'default' }}><span className={'cc-wf-room-dot' + (r.engineeringDpDone ? ' ok' : '')}><Ic n={r.engineeringDpDone ? 'check' : 'zap'} s={11} /></span><span className="cc-wf-room-n">{r.name}</span>
                  <label className="cc-wf-check" style={{ marginLeft: 'auto', fontSize: 12.5, display: 'inline-flex', gap: 6, alignItems: 'center' }}><input type="checkbox" checked={!!r.engineeringDpDone} onChange={(e) => done(r.id, e.target.checked)} />внесено по ДП</label></div>
                <div className="cc-wf-room-b"><div className="cc-wf-counters">
                  {ENG_KINDS.map(([k, l]) => <Counter key={k} label={l} value={get(k)} onChange={(v) => setCount(r.id, k, v)} />)}
                </div></div>
              </div>
            );
          })}
        </div>
      </Card>
    );
  }

  /* условия «Далее» для этих этапов — используются мастером */
  /* обязательные поля (помечены * в форме); всё остальное — по желанию и не блокирует «Далее».
     Элемент списка: { k: ключ поля для подсветки/скролла, l: подпись } */
  const M = (k, l) => ({ k, l });
  function gateClient(it) {
    const m = []; const np = it.nameParts || splitName(it.clientName);
    if (!has(it.clientName) || !(np.last || '').trim() || !(np.first || '').trim()) m.push(M('name', 'фамилия и имя'));
    if (!has(it.clientPhone)) m.push(M('phone', 'телефон'));
    return m;
  }
  function gateObject(it) {
    const m = [];
    if (!has((it.city || '').trim())) m.push(M('city', 'город'));
    if (!has(it.address)) m.push(M('address', 'адрес'));
    if (it.housingType !== 'house' && !has((it.apt || '').trim())) m.push(M('apt', it.housingType === 'commercial' ? 'помещение / офис №' : 'квартира №'));
    if (!has(it.housingType)) m.push(M('housing', 'жильё'));
    if (!has(it.objectType)) m.push(M('objectType', 'новостройка / вторичка'));
    if (!has(it.finishState)) m.push(M('finish', 'отделка сейчас'));
    if (!has(it.roomsCount)) m.push(M('rooms', 'комнат'));
    const area = R ? R.deriveArea(it) : { value: null }; if (!(area.value > 0)) m.push(M('area', 'площадь-ориентир'));
    return m;
  }
  function gateRepair(it) {
    const p = it.prefs || {}; const m = [];
    if (!has(it.repairType)) m.push(M('repairType', 'тип ремонта'));
    if (!has(p.finishTier)) m.push(M('tier', 'уровень отделки'));
    if (!it.dpDecided && it.requiresDesign == null) m.push(M('dp', 'нужен ли дизайн-проект'));
    return m;
  }
  function gateMeasure(it) {
    const rooms = it.rooms || []; const ms = it.measureSession || {}; const m = [];
    if (!has(it.measurementDate)) m.push(M('measureDate', 'дата замера'));
    if (!has((it.measuredBy || '').trim())) m.push(M('measurer', 'замерщик'));
    if (!rooms.length) m.push(M('measure', 'хотя бы одно помещение в мастере замера'));
    else if (ms.status !== 'submitted') m.push(M('measure', '«Отправить замер» на шаге «Итог»'));
    return m;
  }

  window.Calc2WizardForms = { ClientStage, ObjectStage, RepairStage, MeasureStage, DpStage, DpEngineeringStage, DpWorksBridge, StageNote, objectBlueprint, presetFor, techIncludes, stageName, gateClient, gateObject, gateRepair, gateMeasure, TECH };
})();
