/* dp-brief-form.jsx — подробный бриф на дизайн-проект: анкета из Конструктора («Брифы ДП», tpl-dp-brief-full v3: семья и быт,
   стиль карточками, атмосфера, референсы, цвет и материалы, антипримеры, планировка, кухня, инженерия, мебель, помещения,
   бюджет и состав ДП). Блоки раскрываются по мере заполнения: открыт один, остальные — свёрнутые карточки со сводкой ответов.
   Два сценария на одном компоненте: дизайнер/менеджер проходит анкету с клиентом онлайн (поп-ап на этапе «Дизайн-проект»),
   либо клиент — сам по уникальной ссылке lk/brief.html?deal=…&t=<token>. Жизненный цикл: заполняется → «Отправить бриф»
   (submitted) → «Согласовать» дизайнером (approved: ссылка клиента закрывается, сделка идёт к смете) → «Переоткрыть».
   Ответы — в FormInstances (общий стейт через Sync) и через ClientIntakeSync.applyDpPrefs в карточку объекта.
   Exports on window: DpBriefForm, DpBriefModal, DpBrief (ensure, schemaFor, prefill, summary, sectionSummary, markIntake) */
(function () {
  const { Badge, Button } = window.DesignSystem_9e0a09;
  const { Ic } = window;
  const { useState, useEffect, useRef, useCallback, useMemo } = React;
  const FI = () => window.FormInstances;
  const Mock = () => window.DpBriefTemplateMock;
  const Sync = () => window.ClientIntakeSync;
  const TEMPLATE = 'tpl-dp-brief-full';
  const STYLE_TO_CARD = { 'Минимализм': 'minimal', 'Тёплый сканди': 'scandi', 'Лофт': 'loft', 'Современная классика': 'classic', 'Япанди': 'japandi', 'Неоклассика': 'classic' };
  const TECH_TO_ELECTRICAL = { smart_home: 'Умный дом', heated_floor: 'Тёплый пол', heated_floor_el: 'Тёплый пол', heated_floor_w: 'Тёплый пол', light: 'Сценарное освещение', power: 'Больше розеток' };
  const TECH_TO_PLUMBING = { install: 'Инсталляция' };
  const uid = (p) => (p || 'x') + Math.random().toString(36).slice(2, 8);
  const isFilled = (v) => !(v == null || v === '' || v === false || (Array.isArray(v) && !v.length));
  const visible = (f, r) => { const M = Mock(); return M && M.fieldVisible ? M.fieldVisible(f, r) : true; };

  /* анкета сделки: создать, если нет (набор анкет по типу объекта), переключить на подробный шаблон */
  function ensure(dealId, intake) {
    const F = FI(); if (!F) return null;
    let inst = F.findInstance(dealId, (i) => i.kind === 'dp_brief');
    if (!inst) { const wrong = F.findInstance(dealId, (i) => /dp-brief/.test(i.templateId || '')); if (wrong) inst = F.updateInstance(dealId, wrong.id, { kind: 'dp_brief', assigneeRole: 'designer' }); }
    if (!inst) {
      const pt = (window.MeasureDealBridge && window.MeasureDealBridge.propertyTypeFor) ? window.MeasureDealBridge.propertyTypeFor(intake) : 'residential';
      F.materializeForPropertyType(dealId, pt);
      inst = F.findInstance(dealId, (i) => i.kind === 'dp_brief');
    }
    const closed = inst && (inst.accessState === 'submitted' || inst.accessState === 'approved');
    if (inst && inst.templateId !== TEMPLATE && !closed && !(inst.responses && Object.keys(inst.responses).length) && Mock() && Mock().getSchema(TEMPLATE)) {
      inst = F.updateInstance(dealId, inst.id, { templateId: TEMPLATE, title: Mock().getSchema(TEMPLATE).title });
    }
    return inst || null;
  }
  function schemaFor(inst, intake) {
    const M = Mock(); if (!M) return null;
    const raw = M.getSchema((inst && inst.templateId) || TEMPLATE) || M.getSchema('tpl-dp-brief-std');
    return M.materializeSchema ? M.materializeSchema(raw, { dealPropertyType: (intake && (intake.propertyType || intake.housingType)) || 'residential' }) : raw;
  }
  /* предзаполнение из этапов «Объект», «Ремонт и пожелания», «Замер»: стиль → карточки, техрешения → инженерия,
     референсы → ссылки, комнаты замера → помещения, состав ДП → dpScope */
  function prefill(schema, intake) {
    const M = Mock(); const it = intake || {}; const p = it.prefs || {}; const b = it.brief || {};
    const r = M && M.defaultResponses ? M.defaultResponses(schema) : {};
    const votes = {}; (p.style || []).forEach((s) => { const c = STYLE_TO_CARD[s]; if (c) votes[c] = 'like'; });
    if (Object.keys(votes).length) r.styleVotes = { ...(r.styleVotes || {}), ...votes };
    if (b.colorPresets && b.colorPresets.length) r.colorPresets = b.colorPresets;
    const el = new Set(r.electrical || []); const pl = new Set(r.plumbing || []);
    (p.tech || []).forEach((t) => { if (TECH_TO_ELECTRICAL[t]) el.add(TECH_TO_ELECTRICAL[t]); if (TECH_TO_PLUMBING[t]) pl.add(TECH_TO_PLUMBING[t]); });
    if (el.size) r.electrical = [...el]; if (pl.size) r.plumbing = [...pl];
    if (b.furnitureKey) r.furnitureKey = b.furnitureKey;
    if (b.budgetFinish) r.budgetFinish = b.budgetFinish;
    if (b.scope && b.scope.length) r.dpScope = b.scope;
    if (p.deadlineDate) r.deadlineNote = 'к ' + p.deadlineDate + (p.deadlineNote && !/^к /.test(p.deadlineNote) ? ' · ' + p.deadlineNote : '');
    if (p.freeText) r.freeText = p.freeText;
    const urls = (it.references || []).filter((x) => x.url).map((x) => x.url);
    if (urls.length) r.referenceUrls = urls;
    if (/животн|кошк|собак/i.test(p.freeText || '')) r.pets = ['Другие'];
    if (Array.isArray(r.rooms) && r.rooms.every((x) => !x || !x.name) && M && M.defaultRoom) {
      const sec = (schema.sections || []).find((s) => s.kind === 'room_group');
      const names = ((it.rooms || []).map((x) => x.name)).filter(Boolean);
      if (sec && names.length) r.rooms = names.map((nm) => ({ ...M.defaultRoom(sec), name: nm }));
    }
    return r;
  }

  /* сводка ответов одной секции — [[подпись, значение]] */
  /* поля-галереи: ссылки в ответах ↔ картинки+ссылки в карточке объекта */
  const GALLERY = { referenceUrls: { key: 'references', label: 'Картинки и ссылки', kind: 'reference' }, antiUrls: { key: 'antiReferences', label: 'Антипримеры: картинки и ссылки', kind: 'anti' } };
  const refsLabel = (list) => { const refs = list || []; if (!refs.length) return ''; const im = refs.filter((x) => x.imageUrl).length; const ln = refs.length - im; return [im ? im + ' картин.' : '', ln ? ln + ' ссыл.' : ''].filter(Boolean).join(' · '); };
  function sectionSummary(sec, responses, intake) {
    const out = []; const r = responses || {}; const M = Mock();
    if (sec.kind === 'style_cards') {
      const v = r[sec.id] || {}; const cards = sec.cards || (M ? M.STYLE_CARDS : []);
      const like = cards.filter((c) => v[c.id] === 'like').map((c) => c.title); const dis = cards.filter((c) => v[c.id] === 'dislike').map((c) => c.title);
      if (like.length) out.push(['Нравится', like.join(', ')]); if (dis.length) out.push(['Не нравится', dis.join(', ')]);
    } else if (sec.kind === 'room_group') {
      const rooms = (Array.isArray(r[sec.id]) ? r[sec.id] : []).filter((x) => x && x.name);
      if (rooms.length) out.push([sec.title, rooms.map((x) => x.name + (x.priority ? ' (' + ({ high: 'важно', medium: 'средне', low: 'не важно' }[x.priority] || x.priority) + ')' : '') + (x.mood && x.mood.length ? ' · ' + x.mood.join(', ') : '') + (x.note ? ': ' + x.note : '')).join('; ')]);
    } else if (sec.fields) {
      sec.fields.forEach((f) => {
        if (!visible(f, r)) return;
        let v = r[f.id];
        if (GALLERY[f.id] && intake) { const l = refsLabel(intake[GALLERY[f.id].key]); if (l) { out.push([GALLERY[f.id].label, l]); return; } }
        if (!isFilled(v)) return;
        if (f.type === 'chip_single' && Array.isArray(f.options)) { const o = f.options.find((x) => x && typeof x === 'object' && x.v === v); v = o ? o.l : v; }
        if (f.type === 'media') v = v.length + ' фото';
        else if (f.type === 'checkbox') v = 'да';
        else if (Array.isArray(v)) v = v.join(', ');
        if (f.allowCustom && f.customId && r[f.customId]) v += ' · ' + r[f.customId];
        out.push([f.label, String(v)]);
      });
    }
    return out;
  }
  /* сводка по всей схеме — для карточки этапа после отправки: [{ id, title, icon, rows: [[k, v]] }] */
  function summary(schema, responses, intake) {
    return (schema.sections || []).map((sec) => ({ id: sec.id || sec.title, title: sec.title, icon: sec.icon, rows: sectionSummary(sec, responses, intake) })).filter((g) => g.rows.length);
  }
  /* заполненность секции: { total, filled } по видимым полям */
  function sectionStats(sec, responses, intake) {
    const r = responses || {}; let total = 0; let filled = 0;
    if (sec.kind === 'style_cards') { total = Math.min(4, (sec.cards || []).length || 4); filled = Math.min(total, Object.values(r[sec.id] || {}).filter(Boolean).length); }
    else if (sec.kind === 'room_group') {
      const items = Array.isArray(r[sec.id]) ? r[sec.id] : []; const fields = sec.fields || [];
      const count = Math.max(items.length, sec.minItems || 1);
      for (let i = 0; i < count; i += 1) fields.forEach((f) => { total += 1; if (isFilled((items[i] || {})[f.id])) filled += 1; });
    } else if (sec.fields) {
      sec.fields.forEach((f) => { if (!visible(f, r)) return; total += 1; const g = GALLERY[f.id]; const v = g && intake && (intake[g.key] || []).length ? intake[g.key] : r[f.id]; if (isFilled(v)) filled += 1; });
    }
    return { total, filled };
  }

  /* applyDpPrefs из клиентского потока переписывает intake.rooms (там это были пожелания по комнатам) и references —
     здесь помещения приходят из замера, а референсы бывают картинками, поэтому эти поля сохраняем и только дополняем */
  function applyAnswers(dealId, responses) {
    const M = Mock(); const S = Sync(); if (!M || !S) return;
    try {
      const cur = S.loadIntake(dealId) || {};
      const form = M.responsesToIntakeForm(responses);
      const next = S.applyDpPrefs(cur, form);
      next.rooms = cur.rooms || [];
      next.briefRooms = form.rooms || [];
      next.brief = { ...(next.brief || {}), extra: form.extra || {}, scope: (form.extra && form.extra.dpScope) || (cur.brief || {}).scope || [] };
      const mergeLinks = (list, urls) => { const have = new Set((list || []).map((x) => x.url).filter(Boolean)); return [...(list || []), ...(urls || []).filter((u) => u && !have.has(u)).map((u, i) => { let host = u; try { host = new URL(u).hostname.replace(/^www\./, ''); } catch (e) {} return { id: 'ref-b-' + Date.now().toString(36) + i, title: host, url: u, source: host }; })]; };
      next.references = mergeLinks(cur.references, form.referenceUrls);
      next.antiReferences = mergeLinks(cur.antiReferences, (form.extra || {}).antiUrls);
      S.saveIntake(dealId, next);
    } catch (e) {}
  }
  function markIntake(dealId, status, progress) {
    /* один источник цифры: прогресс анкеты пишется в карточку как есть — списки, задачи и процесс ДП показывают одно и то же */
    try {
      const it = window.loadSiteIntake ? window.loadSiteIntake(dealId) : null; if (!it || !window.saveSiteIntake) return;
      const prev = (it.clientForms || {}).dpPrefs || {};
      const now = new Date().toISOString();
      window.saveSiteIntake(dealId, { ...it, clientForms: { ...(it.clientForms || {}), dpPrefs: { ...prev, status, progress, lastSavedAt: now, submittedAt: status === 'submitted' ? now : (status === 'in_progress' ? null : prev.submittedAt), approvedAt: status === 'approved' ? now : null } } });
      window.dispatchEvent(new Event('remontpro:intake-updated'));
    } catch (e) {}
  }
  /* перекрёстные задачи по состоянию брифа: дизайнер (заполнить → согласовать → концепция), клиент (по ссылке), менеджер (маршрут) */
  function syncTasks(dealId, inst, intake) {
    const F = FI(); if (!F || !F.upsertTask || !inst) return;
    const it = intake || (window.loadSiteIntake ? window.loadSiteIntake(dealId) : {}) || {};
    const who = [it.clientName, it.address].filter(Boolean).join(' · ');
    const st = inst.accessState;
    const dealUrl = 'designer-design.html?deal=' + encodeURIComponent(dealId);
    const clientUrl = briefLink(dealId, inst);
    const openD = (title, sub) => { const cur = F.findTaskByKey(dealId, 'dp_brief'); if (cur && cur.status === 'open' && cur.title === title && cur.sub === sub) return; F.upsertTask(dealId, 'dp_brief', { kind: 'dp_brief', assigneeRole: 'designer', title, sub, url: dealUrl, sameTab: true, actionLabel: 'Открыть бриф' }); };
    if (st === 'pending' || st === 'active' || st === 'in_progress') {
      openD('Бриф на дизайн-проект · ' + (st === 'pending' ? 'заполнить с клиентом' : 'заполняется · ' + (inst.progressPct || 0) + '%'), who);
      if (st !== 'pending') { const c = F.findTaskByKey(dealId, 'dp_brief_client'); if (!c || c.status !== 'open' || c.url !== clientUrl) F.upsertTask(dealId, 'dp_brief_client', { kind: 'dp_brief_client', assigneeRole: 'client', title: 'Заполнить бриф на дизайн-проект', sub: who, url: clientUrl, actionLabel: 'Открыть бриф' }); }
      F.completeTaskByKey(dealId, 'dp_concept');
    } else if (st === 'submitted') {
      openD('Согласовать бриф на дизайн-проект', who);
      F.completeTaskByKey(dealId, 'dp_brief_client');
      F.completeTaskByKey(dealId, 'dp_concept');
    } else if (st === 'approved') {
      F.completeTaskByKey(dealId, 'dp_brief');
      F.completeTaskByKey(dealId, 'dp_brief_client');
      const c = F.findTaskByKey(dealId, 'dp_concept');
      if (window.DpPipeline) window.DpPipeline.ensure(dealId);
      if (!c || c.status !== 'open') F.upsertTask(dealId, 'dp_concept', { kind: 'dp_concept', assigneeRole: 'designer', title: 'Разработать концепцию дизайн-проекта', sub: who + ' · бриф согласован', url: 'designer-design.html?deal=' + encodeURIComponent(dealId), sameTab: true, actionLabel: 'Открыть' });
      if (F.pushRouteOnce) F.pushRouteOnce({ dealId, type: 'dp_queue', title: 'Бриф согласован · ДП в работу', requiresDesign: true }, 'brief-approved-' + (inst.approvedAt || ''));
    }
  }
  /* сделки с дизайн-проектом — для кабинета дизайнера: карточка объекта + бриф + смета */
  function listDeals() {
    const F = FI(); const out = [];
    try {
      for (let i = 0; i < localStorage.length; i += 1) {
        const k = localStorage.key(i);
        if (!k || k.indexOf('remontpro_site_intake_') !== 0) continue;
        const dealId = k.slice('remontpro_site_intake_'.length);
        const it = window.loadSiteIntake ? window.loadSiteIntake(dealId) : null;
        if (!it || !it.requiresDesign) continue;
        if (!it.clientName && !it.address) continue;
        const inst = F ? F.findInstance(dealId, (x) => x.kind === 'dp_brief') : null;
        let est = null; try { est = JSON.parse(localStorage.getItem('calc2_estimate_v2_' + dealId) || 'null'); } catch (e) {}
        const schema = inst ? schemaFor(inst, it) : null;
        const stats = schema ? (schema.sections || []).map((s) => sectionStats(s, inst.responses || {}, it)) : [];
        const total = stats.reduce((a, s) => a + s.total, 0); const filled = stats.reduce((a, s) => a + s.filled, 0);
        out.push({
          dealId, intake: it, inst, est,
          client: it.clientName || '—', address: [it.address, it.apt ? 'кв. ' + it.apt : ''].filter(Boolean).join(', ') || '—',
          state: inst ? inst.accessState : 'none', progress: total ? Math.round((filled / total) * 100) : 0,
          blocks: stats.filter((s) => s.filled > 0).length, blocksTotal: stats.length,
          estStatus: est ? est.status : '', updatedAt: (inst && (inst.approvedAt || inst.submittedAt || inst.updatedAt)) || it.updatedAt || '',
        });
      }
    } catch (e) {}
    return out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
  }
  function briefLink(dealId, inst) {
    try { const u = new URL('brief.html', window.location.href); u.searchParams.set('deal', dealId); if (inst && inst.linkToken) u.searchParams.set('t', inst.linkToken); return u.toString(); } catch (e) { return 'brief.html?deal=' + dealId; }
  }
  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: 'image', local: true }));
  }

  /* ─── секция «Референсы» внутри анкеты: картинки и ссылки живут в карточке объекта (intake.references — общие с этапом
     «Ремонт»), пояснение «что нравится» — в ответах брифа ─── */
  function RefsSection({ dealId, intake, responses, patchTop, readonly, sec, urlsField, title }) {
    const g = GALLERY[urlsField || 'referenceUrls'] || GALLERY.referenceUrls;
    const refs = (intake && intake[g.key]) || [];
    const [link, setLink] = useState('');
    const [busy, setBusy] = useState(false);
    const [lb, setLb] = useState(null);
    const Lightbox = window.FeLightbox;
    const noteField = (sec.fields || []).find((f) => f.type === 'textarea');
    const saveRefs = (next) => {
      try { const it = window.loadSiteIntake(dealId) || {}; window.saveSiteIntake(dealId, { ...it, [g.key]: next }); window.dispatchEvent(new Event('remontpro:intake-updated')); } catch (e) {}
      patchTop(g.key === 'references' ? 'referenceUrls' : 'antiUrls', next.filter((x) => x.url).map((x) => x.url));
    };
    const addLink = () => {
      const u = link.trim(); if (!u) return;
      const full = u.startsWith('http') ? u : 'https://' + u;
      let host = full; try { host = new URL(full).hostname.replace(/^www\./, ''); } catch (e) {}
      saveRefs([...refs, { id: uid('ref'), title: host, url: full, 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: g.kind }); saveRefs([...refs, ...up.map((f) => ({ id: uid('ref'), title: f.name, imageUrl: f.url, source: f.local ? 'эта сессия' : 'фото' }))]); }
      finally { setBusy(false); }
    };
    const images = refs.filter((r) => r.imageUrl); const links = refs.filter((r) => !r.imageUrl && r.url);
    return (
      <div className="dpb-refs">
        {title && <div className="fe-label dpb-refs-title">{title}</div>}
        {!readonly && (
          <div className="dpb-refs-add">
            <button type="button" className="cc-ir-btn" 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="Ссылка: pinterest.com/…, behance.net/…" onChange={(e) => setLink(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addLink(); } }} />
              <button type="button" className="cc-ir-btn ghost" onClick={addLink}><Ic n="link" s={14} />Добавить</button>
            </div>
          </div>
        )}
        {images.length > 0 && (
          <div className="dpb-refs-grid">
            {images.map((r, i) => (
              <div key={r.id} className="dpb-ref">
                <button type="button" className="dpb-ref-img" style={{ backgroundImage: 'url(' + r.imageUrl + ')' }} onClick={() => setLb({ images: images.map((x) => x.imageUrl), idx: i, title: r.title || 'Референс' })} aria-label={'Смотреть ' + (r.title || '')} />
                {!readonly && <button type="button" className="cc-wf-ref-x" onClick={() => saveRefs(refs.filter((x) => x.id !== r.id))} aria-label="Убрать"><Ic n="x" s={12} /></button>}
              </div>
            ))}
          </div>
        )}
        {links.length > 0 && (
          <div className="dpb-refs-links">
            {links.map((r) => (
              <div key={r.id} className="dpb-ref-link">
                <a href={r.url} target="_blank" rel="noreferrer"><Ic n="external-link" s={13} />{r.title || r.url}</a>
                {!readonly && <button type="button" className="cc-wf-ref-x static" onClick={() => saveRefs(refs.filter((x) => x.id !== r.id))} aria-label="Убрать"><Ic n="x" s={12} /></button>}
              </div>
            ))}
          </div>
        )}
        {!refs.length && <div className="dpb-empty">{g.key === 'antiReferences' ? 'Что точно не нравится — картинки с телефона или ссылки: дизайнеру это так же важно, как референсы.' : 'Пока нет референсов — добавьте картинки с телефона или ссылки на Pinterest / Behance.'}</div>}
        {noteField && (
          <label className="fe-field fe-field--wide" style={{ marginTop: 12 }}>
            <span className="fe-label">{noteField.label}</span>
            <textarea className="fe-input fe-textarea" rows={3} disabled={readonly} value={responses[noteField.id] || ''} placeholder={noteField.placeholder || ''} onChange={(e) => patchTop(noteField.id, e.target.value)} />
          </label>
        )}
        {Lightbox && <Lightbox lb={lb} onClose={() => setLb(null)} onIdx={(idx) => setLb((c) => (c ? { ...c, idx } : c))} />}
      </div>
    );
  }

  const STATUS = {
    pending: ['не начат', 'neutral'], active: ['ссылка отправлена', 'info'], in_progress: ['заполняется', 'info'],
    submitted: ['отправлен · ждёт согласования', 'warning'], approved: ['согласован', 'success'], expired: ['истёк', 'neutral'],
  };

  /* ─── анкета: карточки-блоки, открыт один, остальные свёрнуты со сводкой; следующий раскрывается по «Далее» ─── */
  function DpBriefForm({ dealId, intake, inst, onInstance, who, layout, onClose }) {
    const F = FI(); const Section = window.FormSection;
    const schema = useMemo(() => schemaFor(inst, intake), [inst && inst.templateId, intake && intake.housingType]);
    const sections = (schema && schema.sections) || [];
    const [responses, setResponses] = useState(() => (inst && inst.responses && Object.keys(inst.responses).length ? inst.responses : prefill(schema, intake)));
    const state = inst ? inst.accessState : 'pending';
    const readonly = !inst || state === 'submitted' || state === 'approved';
    /* после отправки бриф читается как презентация (DpBriefView); «Показать анкету» — обратно к форме только для чтения */
    const [showForm, setShowForm] = useState(false);
    const View = window.DpBriefView;
    useEffect(() => { if (!readonly) setShowForm(false); }, [readonly]);
    const isClient = who === 'client';
    const stats = sections.map((sec) => sectionStats(sec, responses, intake));
    const done = stats.map((s) => s.filled > 0);
    const [open, setOpen] = useState(() => { if (readonly) return -1; const i = done.findIndex((d) => !d); return i < 0 ? sections.length - 1 : i; });
    const [reached, setReached] = useState(() => Math.max(open, done.lastIndexOf(true)));
    const total = stats.reduce((a, s) => a + s.total, 0); const filled = stats.reduce((a, s) => a + s.filled, 0);
    const progress = total ? Math.min(100, Math.round((filled / total) * 100)) : 0;
    const doneCount = done.filter(Boolean).length;
    const timer = useRef(null);
    const secRefs = useRef({});
    const persist = useCallback((next) => {
      if (!F || !inst || readonly) return;
      clearTimeout(timer.current);
      timer.current = setTimeout(() => {
        const fresh = F.saveFieldResponses(dealId, inst.id, next, schema);
        if (inst.accessState === 'pending' || inst.accessState === 'active') F.updateInstance(dealId, inst.id, { accessState: 'in_progress' });
        applyAnswers(dealId, next);
        markIntake(dealId, 'in_progress', F.computeSchemaProgress(schema, next));
        onInstance && onInstance(F.findInstance(dealId, (i) => i.id === inst.id) || fresh);
      }, 350);
    }, [dealId, inst && inst.id, inst && inst.accessState, schema, readonly]);
    const patchTop = useCallback((id, val) => setResponses((prev) => { const next = { ...prev, [id]: val }; persist(next); return next; }), [persist]);
    const patchSection = useCallback((sid, val) => setResponses((prev) => { const next = { ...prev, [sid]: val }; persist(next); return next; }), [persist]);
    const goTo = (i) => {
      setOpen(i); setReached((r) => Math.max(r, i));
      setTimeout(() => { const el = secRefs.current[i]; if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 30);
    };
    const flush = () => { clearTimeout(timer.current); if (F && inst && !readonly) F.saveFieldResponses(dealId, inst.id, responses, schema); };
    const submit = () => {
      if (!F || !inst) return;
      flush();
      if (inst.accessState === 'pending' || inst.accessState === 'active') F.updateInstance(dealId, inst.id, { accessState: 'in_progress' });
      // submitDpBrief внутри вызывает aggregateDpBrief → applyDpPrefs, который затирает помещения замера — восстанавливаем
      const before = (window.loadSiteIntake && window.loadSiteIntake(dealId)) || {};
      const fresh = F.submitDpBrief(dealId, inst.id, responses, schema);
      try { const it = window.loadSiteIntake(dealId); window.saveSiteIntake(dealId, { ...it, rooms: before.rooms || [], references: before.references || [] }); } catch (e) {}
      applyAnswers(dealId, responses);
      markIntake(dealId, 'submitted', 100);
      setOpen(-1);
      const after = fresh || F.findInstance(dealId, (i) => i.id === inst.id);
      syncTasks(dealId, after, intake);
      onInstance && onInstance(after);
    };
    const approve = () => {
      if (!F || !inst) return;
      const fresh = F.approveDpBrief ? F.approveDpBrief(dealId, inst.id) : F.updateInstance(dealId, inst.id, { accessState: 'approved', approvedAt: new Date().toISOString() });
      markIntake(dealId, 'approved', 100);
      syncTasks(dealId, fresh, intake);
      onInstance && onInstance(fresh);
    };
    const reopen = () => {
      if (!F || !inst) return;
      const fresh = F.reopenDpBrief ? F.reopenDpBrief(dealId, inst.id) : F.updateInstance(dealId, inst.id, { accessState: 'in_progress', submittedAt: null, approvedAt: null });
      markIntake(dealId, 'in_progress', progress);
      syncTasks(dealId, fresh, intake);
      const i = done.findIndex((d) => !d); setOpen(i < 0 ? 0 : i);
      setShowForm(false);
      onInstance && onInstance(fresh);
    };
    if (!schema || !Section) return <div className="fe-empty">Шаблон брифа не загружен (Конструктор → Брифы ДП).</div>;
    if (readonly && inst && View && !showForm) {
      return (
        <div className={'dpb dpb--' + (layout || 'page') + ' is-readonly dpb--view'}>
          <View dealId={dealId} intake={intake} inst={inst} schema={schema} responses={responses} who={who}
            onEdit={isClient ? null : reopen} onApprove={!isClient && state === 'submitted' ? approve : null} onClose={onClose} onShowForm={() => setShowForm(true)} />
        </div>
      );
    }
    const [stLabel, stTone] = STATUS[state] || [state, 'neutral'];
    const nextIdx = open >= 0 && open < sections.length - 1 ? open + 1 : -1;
    const sub = isClient
      ? (readonly ? 'Ответы у дизайнера. Если что-то нужно поменять — напишите ему, он переоткроет анкету.' : 'Отвечайте, как чувствуете — это ориентир для дизайнера, не контракт. Сохраняется само, можно заполнять частями.')
      : (state === 'approved' ? 'Бриф согласован — ссылка клиента закрыта, идём к составу работ.' : state === 'submitted' ? 'Проверьте ответы и согласуйте — после этого ссылка клиента закроется.' : 'Проходите блоки вместе с клиентом или отправьте ему ссылку — ответы общие. Сохраняется само.');
    const who2 = [intake && intake.address, intake && intake.clientName].filter(Boolean).join(' · ');
    return (
      <div className={'dpb dpb--' + (layout || 'page') + (readonly ? ' is-readonly' : '')}>
        <div className="dpb-head">
          <div className="dpb-head-l">
            <div className="dpb-title">{schema.title || 'Бриф на дизайн-проект'}</div>
            <div className="dpb-sub">{who2 ? who2 + ' — ' : ''}{sub}</div>
          </div>
          <div className="dpb-head-r">
            <div className="dpb-prog" title={filled + ' из ' + total + ' ответов'}>
              <div className="dpb-prog-bar"><i style={{ width: progress + '%' }} /></div>
              <span className="t-num">{doneCount}/{sections.length} блоков · {progress}%</span>
            </div>
            <Badge tone={stTone} dot>{stLabel}</Badge>
            {onClose && <button type="button" className="dpb-x" onClick={() => { flush(); onClose(); }} aria-label="Закрыть"><Ic n="x" s={18} /></button>}
          </div>
        </div>
        <div className="dpb-layout">
          <aside className="dpb-rail" aria-label="Блоки анкеты">
            {sections.map((sec, i) => (
              <button key={sec.id || i} type="button" className={'dpb-rail-it' + (i === open ? ' is-open' : '') + (done[i] ? ' is-done' : '') + (i > reached && !readonly ? ' is-ahead' : '')} onClick={() => goTo(i)}>
                <span className="dpb-rail-n">{done[i] ? <Ic n="check" s={12} /> : i + 1}</span>
                <span className="dpb-rail-t">{sec.title}</span>
                <span className="dpb-rail-c t-num">{stats[i].filled}/{stats[i].total}</span>
              </button>
            ))}
          </aside>
          <div className="dpb-main">
            {sections.map((sec, i) => {
              const isOpen = i === open;
              const rows = !isOpen ? sectionSummary(sec, responses, intake) : [];
              const ahead = i > reached && !readonly;
              return (
                <article key={sec.id || i} ref={(el) => { secRefs.current[i] = el; }} className={'dpb-sec' + (isOpen ? ' is-open' : '') + (done[i] ? ' is-done' : '') + (ahead ? ' is-ahead' : '')}>
                  <button type="button" className="dpb-sec-h" onClick={() => (isOpen ? setOpen(-1) : goTo(i))} aria-expanded={isOpen}>
                    <span className="dpb-sec-n">{done[i] && !isOpen ? <Ic n="check" s={13} /> : <Ic n={sec.icon || 'circle'} s={15} />}</span>
                    <span className="dpb-sec-tt">
                      <b>{sec.title}</b>
                      {isOpen && (sec.hint || sec.subtitle) && <span className="dpb-sec-hint">{sec.hint || sec.subtitle}</span>}
                      {!isOpen && rows.length > 0 && <span className="dpb-sec-sum">{rows.slice(0, 5).map(([k, v]) => <span key={k} className="dpb-sum-chip"><i>{k}:</i> {v.length > 60 ? v.slice(0, 58) + '…' : v}</span>)}{rows.length > 5 && <span className="dpb-sum-chip">+{rows.length - 5}</span>}</span>}
                      {!isOpen && !rows.length && <span className="dpb-sec-hint">{ahead ? 'откроется дальше — можно сразу перейти' : (readonly ? 'без ответов' : (sec.hint || 'не заполнено'))}</span>}
                    </span>
                    <span className="dpb-sec-c t-num">{stats[i].filled}/{stats[i].total}</span>
                    <span className="dpb-sec-chev"><Ic n={isOpen ? 'chevron-up' : 'chevron-down'} s={16} /></span>
                  </button>
                  {isOpen && (
                    <div className="dpb-sec-b">
                      {sec.id === 'references' && <RefsSection dealId={dealId} intake={intake} responses={responses} patchTop={patchTop} readonly={readonly} sec={sec} urlsField="referenceUrls" />}
                      {sec.id === 'anti' && <Section section={{ ...sec, fields: (sec.fields || []).filter((f) => f.id !== 'antiUrls') }} responses={responses} canEdit={!readonly} onPatchTop={patchTop} onPatchSection={patchSection} hideTitle upload={{ pid: dealId, kind: 'brief' }} />}
                      {sec.id === 'anti' && <RefsSection dealId={dealId} intake={intake} responses={responses} patchTop={patchTop} readonly={readonly} sec={{ ...sec, fields: [] }} urlsField="antiUrls" title="Антипримеры — картинки и ссылки" />}
                      {sec.id !== 'references' && sec.id !== 'anti' && <Section section={sec} responses={responses} canEdit={!readonly} onPatchTop={patchTop} onPatchSection={patchSection} hideTitle upload={{ pid: dealId, kind: 'brief' }} />}
                      <div className="dpb-sec-f">
                        {i > 0 && <Button variant="secondary" size="md" onClick={() => goTo(i - 1)}>Назад</Button>}
                        <span className="dpb-sec-fs" />
                        {nextIdx >= 0
                          ? <Button variant="accent" size="md" className="dpb-cta" onClick={() => goTo(nextIdx)}>Далее: {sections[nextIdx].title} →</Button>
                          : (!readonly && <Button variant="accent" size="md" className="dpb-cta" onClick={submit}>Отправить бриф</Button>)}
                      </div>
                    </div>
                  )}
                </article>
              );
            })}
          </div>
        </div>
        <div className="dpb-foot">
          {!readonly && <Button variant="accent" size="md" className="dpb-cta" onClick={submit}>Отправить бриф</Button>}
          {!readonly && <span className="dpb-hint">Обязательных полей нет — отправляйте, когда картина ясна. Заполнено {progress}%.</span>}
          {readonly && state === 'submitted' && !isClient && <Button variant="accent" size="md" className="dpb-cta" onClick={approve}><Ic n="check" s={14} />Согласовать бриф</Button>}
          {readonly && state === 'submitted' && !isClient && <span className="dpb-hint">Согласование закрывает ссылку клиента и открывает этап «Работы».</span>}
          {readonly && !isClient && <Button variant="secondary" size="md" onClick={reopen}>Переоткрыть</Button>}
          {readonly && View && <Button variant="ghost" size="md" iconLeft={<Ic n="presentation" s={14} />} onClick={() => setShowForm(false)}>Презентация</Button>}
          {readonly && isClient && <span className="dpb-hint">{state === 'approved' ? 'Бриф согласован дизайнером.' : 'Бриф отправлен дизайнеру — спасибо!'}</span>}
          {onClose && <Button variant="secondary" size="md" onClick={() => { flush(); onClose(); }}>Закрыть</Button>}
        </div>
      </div>
    );
  }

  /* поп-ап анкеты для этапа «Дизайн-проект» */
  function DpBriefModal({ onClose, ...props }) {
    useEffect(() => {
      const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); };
      window.addEventListener('keydown', onKey);
      const prev = document.body.style.overflow; document.body.style.overflow = 'hidden';
      return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
    }, [onClose]);
    return (
      <div className="dpb-modal-bg" onMouseDown={(e) => { if (e.target === e.currentTarget && onClose) onClose(); }}>
        <div className="dpb-modal" role="dialog" aria-modal="true" aria-label="Бриф на дизайн-проект">
          <DpBriefForm {...props} layout="modal" onClose={onClose} />
        </div>
      </div>
    );
  }

  window.DpBriefForm = DpBriefForm;
  window.DpBriefModal = DpBriefModal;
  window.DpBrief = { TEMPLATE, ensure, schemaFor, prefill, summary, sectionSummary, sectionStats, markIntake, briefLink, STATUS, syncTasks, listDeals };
})();
