/* dp-pipeline-panel.jsx — UI дизайн-проекта по этапам. DpStagesPanel (дизайнер): степпер этапов, файлы/ссылки, заметка,
   спецификация-таблица, «Отправить клиенту» / «Готово», комментарии и правки клиента. DpClientApprovals (клиент):
   этапы на согласовании — галерея, «Согласовать» / «Нужны правки» с комментарием; утверждённые — альбом.
   DpProgressCard — компактная сводка для мастера/карточки сделки. Данные — window.DpPipeline.
   Exports on window: DpStagesPanel, DpClientApprovals, DpProgressCard, DpAlbum */
(function () {
  const { Badge, Button } = window.DesignSystem_9e0a09;
  const { Ic } = window;
  const { useState, useEffect, useMemo, useRef } = React;
  const P = () => window.DpPipeline;
  const fmtAt = (s) => { const d = new Date(s); return isNaN(d) ? '' : d.toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }).replace('.', ''); };
  const rub = (n) => Math.round(n || 0).toLocaleString('ru-RU') + ' ₽';
  /* категория позиции спецификации для чтения: показываем понятный label, но старые свободные значения,
     которые ни к чему не резолвятся (specCategory отдаёт для них категорию 'other'), оставляем как есть */
  function specCatLabel(v) {
    if (!v) return v;
    const cat = window.ConstructorData && window.ConstructorData.specCategory ? window.ConstructorData.specCategory(v) : null;
    if (!cat) return v;
    if (cat.id === 'other') {
      const norm = String(v).trim().toLowerCase();
      const isReallyOther = norm === cat.id || norm === cat.label.toLowerCase() || norm === cat.label.split(' (')[0].toLowerCase();
      if (!isReallyOther) return v;
    }
    return cat.label;
  }
  function usePipeline(dealId) {
    const [tick, setTick] = useState(0);
    useEffect(() => { const up = () => setTick((x) => x + 1); ['obitel:dp-pipeline', 'storage', 'obitel:sync', 'remontpro:forms-updated'].forEach((e) => window.addEventListener(e, up)); return () => ['obitel:dp-pipeline', 'storage', 'obitel:sync', 'remontpro:forms-updated'].forEach((e) => window.removeEventListener(e, up)); }, []);
    return useMemo(() => (P() && dealId ? P().ensure(dealId) : null), [dealId, tick]);
  }
  function useTick() { const [t, setT] = useState(0); useEffect(() => { const up = () => setT((x) => x + 1); ['obitel:dp-pipeline', 'storage', 'obitel:sync', 'remontpro:forms-updated', 'remontpro:intake-updated'].forEach((e) => window.addEventListener(e, up)); return () => ['obitel:dp-pipeline', 'storage', 'obitel:sync', 'remontpro:forms-updated', 'remontpro:intake-updated'].forEach((e) => window.removeEventListener(e, up)); }, []); return t; }
  async function pickFiles(accept, multiple) { return new Promise((resolve) => { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = accept || 'image/*,application/pdf'; 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 })); }

  /* галерея файлов этапа */
  function Items({ items, onRemove, readonly }) {
    const [lb, setLb] = useState(null); const Lightbox = window.FeLightbox;
    const imgs = items.filter((x) => x.kind === 'image'); const files = items.filter((x) => x.kind !== 'image');
    if (!items.length) return <div className="dpj-empty">Файлов пока нет.</div>;
    return (
      <React.Fragment>
        {imgs.length > 0 && <div className="dpj-gallery">{imgs.map((x, i) => (
          <div key={x.id} className="dpj-img-w">
            <button type="button" className="dpj-img" style={{ backgroundImage: 'url(' + x.url + ')' }} onClick={() => setLb({ images: imgs.map((y) => y.url), idx: i, title: x.name })} title={x.name} />
            <span className="dpj-img-cap">v{x.version} · {x.name}</span>
            {!readonly && <button type="button" className="cc-wf-ref-x" onClick={() => onRemove(x.id)} aria-label="Убрать"><Ic n="x" s={12} /></button>}
          </div>))}</div>}
        {files.length > 0 && <div className="dpj-files">{files.map((x) => (
          <div key={x.id} className="dpj-file"><Ic n={x.kind === 'pdf' ? 'file-text' : 'link'} s={14} /><a href={x.url} target="_blank" rel="noreferrer">{x.name}</a><span className="dpj-muted">v{x.version} · {fmtAt(x.at)}</span>{!readonly && <button type="button" className="cc-wf-ref-x static" onClick={() => onRemove(x.id)} aria-label="Убрать"><Ic n="x" s={12} /></button>}</div>))}</div>}
        {Lightbox && <Lightbox lb={lb} onClose={() => setLb(null)} onIdx={(idx) => setLb((c) => (c ? { ...c, idx } : c))} />}
      </React.Fragment>
    );
  }
  function Comments({ list }) {
    if (!list || !list.length) return null;
    return <div className="dpj-comments">{list.slice().reverse().map((c) => <div key={c.id} className={'dpj-comment' + (c.kind === 'changes' ? ' is-changes' : '')}><div className="dpj-comment-m">{c.role === 'client' ? 'Клиент' : c.role === 'designer' ? 'Дизайнер' : (c.by || '')} · {fmtAt(c.at)}{c.kind === 'changes' && ' · запрос правок'}</div><div>{c.text}</div></div>)}</div>;
  }
  /* этапы ремонта для колонки «К этапу» (спека волны 2, раздел 4.5): этапы каталога без фазы 0 — их id совпадают с этапами проекта */
  function repairStages() {
    const CD = window.ConstructorData; let cat = [];
    try { cat = CD && CD.loadCatalog ? CD.loadCatalog() : []; } catch (e) { cat = []; }
    return (cat || []).filter((ph) => ph.id !== 'phase_design').flatMap((ph) => (ph.stages || []).map((st) => ({ id: st.id, name: st.name })));
  }
  const BUYER_L = { us: 'Мы', client: 'Клиент' };
  /* спецификация */
  function SpecTable({ dealId, rows, readonly, rooms }) {
    const D = P();
    const CD = window.ConstructorData;
    const SPEC_CATS = (CD && CD.SPEC_CATEGORIES) || [];
    const catOf = (v) => (CD && CD.specCategory ? CD.specCategory(v) : null);
    const total = rows.reduce((a, r) => a + (+r.qty || 0) * (+r.price || 0), 0);
    const upd = (id, k, v) => D.specUpdate(dealId, id, { [k]: v });
    const stages = useMemo(repairStages, []);
    const stageL = (id) => (stages.find((x) => x.id === id) || {}).name || '';
    return (
      <div className="dpj-spec">
        <div className="sd-scroll"><table className="dpj-tbl">
          <thead><tr><th>Помещение</th><th>Категория</th><th title="«Клиент» — покупает сам: в смету и закупку позиция не идёт">Кто покупает</th><th title="Этап ремонта, к старту которого позиция нужна на объекте">К этапу</th><th>Поставка, дн</th><th>Позиция</th><th>Артикул / ссылка</th><th className="num">Кол-во</th><th>Ед.</th><th className="num">Цена</th><th className="num">Сумма</th>{!readonly && <th></th>}</tr></thead>
          <tbody>
            {rows.map((r) => {
              const cat = catOf(r.category);
              const isLegacyCat = r.category && !SPEC_CATS.some((c) => c.id === r.category || c.label === r.category);
              const leadVal = r.lead != null && r.lead !== '' ? r.lead : (cat ? cat.lead : '');
              return (
              <tr key={r.id}>
                <td>{readonly ? r.room : <input list="dpj-rooms" className="dpj-in" value={r.room} onChange={(e) => upd(r.id, 'room', e.target.value)} placeholder="Гостиная" />}</td>
                <td>{readonly ? specCatLabel(r.category) : <select className="dpj-in" value={r.category} onChange={(e) => upd(r.id, 'category', e.target.value)}>{isLegacyCat && <option value={r.category}>{r.category}</option>}{SPEC_CATS.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}</select>}</td>
                <td>{readonly ? BUYER_L[r.buyer === 'client' ? 'client' : 'us'] : <select className="dpj-in dpj-in--n" value={r.buyer === 'client' ? 'client' : 'us'} onChange={(e) => upd(r.id, 'buyer', e.target.value)}><option value="us">Мы</option><option value="client">Клиент</option></select>}</td>
                <td>{readonly ? stageL(r.needStage) : <select className="dpj-in" value={r.needStage || ''} onChange={(e) => upd(r.id, 'needStage', e.target.value)}><option value="">— по категории —</option>{stages.map((x) => <option key={x.id} value={x.id}>{x.name}</option>)}</select>}</td>
                <td className="num">{readonly ? leadVal : <input type="number" inputMode="numeric" className="dpj-in dpj-in--n" value={leadVal} onChange={(e) => upd(r.id, 'lead', e.target.value)} />}</td>
                <td>{readonly ? r.name : <input className="dpj-in dpj-in--w" value={r.name} onChange={(e) => upd(r.id, 'name', e.target.value)} placeholder="Керамогранит 60×120, Estima…" />}</td>
                <td>{readonly ? (r.url ? <a href={r.url} target="_blank" rel="noreferrer">{r.article || 'ссылка'}</a> : r.article) : <input className="dpj-in" value={r.article} onChange={(e) => upd(r.id, 'article', e.target.value)} placeholder="арт. / URL" onBlur={(e) => { const v = e.target.value; if (/^https?:\/\//.test(v)) upd(r.id, 'url', v); }} />}</td>
                <td className="num">{readonly ? r.qty : <input type="number" inputMode="decimal" className="dpj-in dpj-in--n" value={r.qty} onChange={(e) => upd(r.id, 'qty', e.target.value)} />}</td>
                <td>{readonly ? r.unit : <select className="dpj-in dpj-in--n" value={r.unit} onChange={(e) => upd(r.id, 'unit', e.target.value)}>{['шт', 'м²', 'м.п.', 'компл', 'уп'].map((u) => <option key={u}>{u}</option>)}</select>}</td>
                <td className="num">{readonly ? rub(r.price) : <input type="number" inputMode="decimal" className="dpj-in dpj-in--n" value={r.price} onChange={(e) => upd(r.id, 'price', e.target.value)} />}</td>
                <td className="num t-num">{rub((+r.qty || 0) * (+r.price || 0))}</td>
                {!readonly && <td><button type="button" className="cc-wf-ref-x static" onClick={() => D.specRemove(dealId, r.id)} aria-label="Убрать"><Ic n="x" s={12} /></button></td>}
              </tr>);})}
            {!rows.length && <tr><td colSpan={12} className="dpj-empty">Позиций нет — добавьте материалы и мебель по помещениям.</td></tr>}
          </tbody>
          <tfoot><tr><td colSpan={10} style={{ textAlign: 'right', fontWeight: 800 }}>Итого{rows.some((r) => r.buyer === 'client') ? ' · мы ' + rub(rows.filter((r) => r.buyer !== 'client').reduce((a, r) => a + (+r.qty || 0) * (+r.price || 0), 0)) + ' · клиент ' + rub(rows.filter((r) => r.buyer === 'client').reduce((a, r) => a + (+r.qty || 0) * (+r.price || 0), 0)) : ''}</td><td className="num t-num" style={{ fontWeight: 800 }}>{rub(total)}</td>{!readonly && <td></td>}</tr></tfoot>
        </table></div>
        {!readonly && <datalist id="dpj-rooms">{(rooms || []).map((r) => <option key={r} value={r} />)}</datalist>}
        {!readonly && <div className="dpj-spec-add">
          <Button variant="secondary" size="sm" iconLeft={<Ic n="plus" s={13} />} onClick={() => D.specAdd(dealId, {})}>Позиция</Button>
          {(rooms || []).length > 0 && <Button variant="ghost" size="sm" onClick={() => rooms.forEach((rm) => D.specAdd(dealId, { room: rm, category: 'Отделка', name: 'Напольное покрытие' }))}>Заготовка по помещениям</Button>}
        </div>}
      </div>
    );
  }

  /* ─── дизайнер: этапы ─── */
  function DpStagesPanel({ dealId, intake, compact, nested }) {
    const D = P(); const d = usePipeline(dealId);
    const [open, setOpen] = useState(null);
    const [busy, setBusy] = useState(false);
    const [link, setLink] = useState('');
    if (!D || !d) return <div className="lk-empty-note">Модуль дизайн-проекта не загружен.</div>;
    const sm = D.summary(dealId);
    const cur = open || (sm.current ? sm.current.id : D.STAGES[0].id);
    const meta = D.STAGES.find((s) => s.id === cur); const s = d.stages[cur];
    const [stLabel, stTone] = D.label(s.status);
    const rooms = ((intake && intake.rooms) || []).map((r) => r.name).filter(Boolean);
    const add = async () => { const files = await pickFiles(meta.accepts.includes('pdf') ? 'image/*,application/pdf' : 'image/*', true); if (!files.length) return; setBusy(true); try { const up = await uploadFiles(files, { pid: dealId, kind: 'dp-' + cur }); D.addItems(dealId, cur, up.map((f) => ({ url: f.url, name: f.name, kind: /pdf$/i.test(f.name) || f.type === 'application/pdf' ? 'pdf' : 'image' })), 'designer'); } finally { setBusy(false); } };
    const addLink = () => { const u = link.trim(); if (!u) return; D.addItems(dealId, cur, [{ url: u.startsWith('http') ? u : 'https://' + u, name: u.replace(/^https?:\/\//, '').slice(0, 60), kind: 'link' }], 'designer'); setLink(''); };
    const nItems = cur === 'spec' ? (s.rows || []).length : s.items.length;
    const hasNote = !!(s.note && s.note.trim());
    const canSend = meta.approver === 'client' && nItems >= Math.max(1, meta.min || 1) && (s.status === 'draft' || s.status === 'changes');
    const canDone = meta.approver === 'designer' && (s.status === 'draft' || s.status === 'changes');
    const idx = D.STAGES.findIndex((x) => x.id === cur);
    const next = D.STAGES.slice(idx + 1).find((x) => d.stages[x.id].status !== 'approved' && d.stages[x.id].status !== 'done') || null;
    const nextRec = sm.current && sm.current.status === 'pending' ? sm.current.id : (D.STAGES.find((x) => d.stages[x.id].status === 'pending') || {}).id; // «следующий» в степпере
    const finished = s.status === 'approved' || s.status === 'done';
    const lastChange = (s.comments || []).filter((c) => c.kind === 'changes').slice(-1)[0];
    const coreDone = D.CORE.every((x) => d.stages[x.id].status === 'approved' || d.stages[x.id].status === 'done');
    const album = (tab) => 'dp-album.html?deal=' + encodeURIComponent(dealId) + '&tab=' + tab;
    const stageRight = (st, x) => {
      if (x.status === 'approved') return 'Утверждено' + (x.approvedAt ? ' · ' + fmtAt(x.approvedAt) : '');
      if (x.status === 'done') return 'Готово' + (x.approvedAt ? ' · ' + fmtAt(x.approvedAt) : '');
      if (x.status === 'review') return 'У клиента с ' + fmtAt(x.sentAt);
      if (x.status === 'changes') return 'Правки клиента';
      if (x.status === 'draft') return st.id === cur ? 'В работе' : 'Можно начинать';
      return st.id === nextRec ? 'Следующий' : 'Не начат';
    };
    const stageCls = (x) => (x.status === 'approved' || x.status === 'done' ? 'is-done' : x.status === 'pending' ? 'is-todo' : 'is-cur');
    // PDF-презентация для кабинета клиента (рядом со слайдами из утверждённых этапов) — DpPipeline.setPresentation
    const pdfUp = async () => { const files = await pickFiles('application/pdf', false); if (!files.length) return; setBusy(true); try { const up = await uploadFiles(files, { pid: dealId, kind: 'dp-presentation' }); if (up[0]) D.setPresentation(dealId, { url: up[0].url, name: up[0].name }); } finally { setBusy(false); } };
    return (
      <div className={'dpj' + (compact ? ' dpj--compact' : '')}>
        <div className="dpj-pdf">
          <Ic n="presentation" s={14} /><b>PDF-презентация</b>
          {d.presentation
            ? <React.Fragment><a href={d.presentation.url} target="_blank" rel="noreferrer">{d.presentation.name}</a><span className="dpj-muted">· {fmtAt(d.presentation.at)}</span><Button variant="ghost" size="sm" disabled={busy} onClick={pdfUp}>Заменить</Button><button type="button" className="cc-wf-ref-x static" onClick={() => D.setPresentation(dealId, null)} aria-label="Убрать презентацию"><Ic n="x" s={12} /></button></React.Fragment>
            : <React.Fragment><span className="dpj-muted">клиент увидит её рядом со слайдами из утверждённых этапов</span><Button variant="secondary" size="sm" disabled={busy} iconLeft={<Ic n="upload" s={13} />} onClick={pdfUp}>{busy ? 'Загружаю…' : 'Загрузить PDF'}</Button></React.Fragment>}
        </div>
        <div className={'rail dpj-rail' + (nested ? ' rail--sub' : '')}>
          {D.STAGES.map((st, i) => { const x = d.stages[st.id]; const isOpen = cur === st.id; const done = x.status === 'approved' || x.status === 'done';
            return (
              <div key={st.id} className={'rail-row ' + stageCls(x) + (isOpen ? ' is-open' : '')}>
                <div className="rail-line"><span className="rail-node">{done ? <Ic n="check" s={14} /> : i + 1}</span><span className="rail-stem" /></div>
                <div className="rail-body">
                  <button type="button" className="rail-head" onClick={() => setOpen(isOpen ? null : st.id)}>
                    <span className="rail-t">{st.title}</span>
                    {!isOpen && <span className="rail-meta">{(x.items || []).length ? (x.items.length + ' файл.') : (st.id === 'spec' && (x.rows || []).length ? (x.rows.length + ' поз.') : '')}</span>}
                    <span className="rail-right">{stageRight(st, x)}</span>
                  </button>
                  {isOpen && <div className="rail-panel">{renderStage()}</div>}
                </div>
              </div>); })}
        </div>
      </div>
    );
    function renderStage() {
      return (
        <div className="dpj-stage">
          <div className="dpj-stage-h">
            <div><div className="dpj-stage-t"><Ic n={meta.icon} s={16} />Что должно получиться<span className="dpj-muted"> · версия {s.version}</span></div><div className="dpj-stage-r">{meta.result}</div></div>
            <Badge tone={stTone} dot>{stLabel}</Badge>
          </div>
          {/* ── что сделать сейчас: по состоянию этапа ── */}
          {(s.status === 'draft' || s.status === 'pending' || s.status === 'changes') && (
            <div className={'dpj-now' + (s.status === 'changes' ? ' is-changes' : '')}>
              <div className="dpj-now-h"><Ic n={s.status === 'changes' ? 'undo-2' : 'list-checks'} s={15} />{s.status === 'changes' ? 'Клиент просит правки — версия ' + s.version : 'Что сделать на этом этапе'}</div>
              {s.status === 'changes' && lastChange && <div className="dpj-now-quote">«{lastChange.text}»<span className="dpj-muted"> · {fmtAt(lastChange.at)}</span></div>}
              <ol className="dpj-todo">
                <li className={nItems >= Math.max(1, meta.min || 1) ? 'is-ok' : ''}><span className="dpj-todo-n">{nItems >= Math.max(1, meta.min || 1) ? <Ic n="check" s={12} /> : '1'}</span><b>{meta.min === 0 ? 'Добавьте материалы' : 'Загрузите материалы'}</b><span>{meta.todo}</span>{nItems > 0 && <em>{nItems} {cur === 'spec' ? 'поз.' : 'файл.'}</em>}</li>
                {meta.approver === 'client' && <li className={hasNote ? 'is-ok' : ''}><span className="dpj-todo-n">{hasNote ? <Ic n="check" s={12} /> : '2'}</span><b>Напишите пояснение для клиента</b><span>что смотреть, какие варианты, на что обратить внимание</span></li>}
                <li><span className="dpj-todo-n">{meta.approver === 'client' ? '3' : '2'}</span><b>{meta.approver === 'client' ? 'Отправьте клиенту на согласование' : 'Отметьте «Этап готов»'}</b><span>{meta.approver === 'client' ? 'клиент увидит этап в кабинете и по ссылке, решит: согласовать или запросить правки' : 'этап закрывает дизайнер' + (cur === 'docs' ? ' — альбом уйдёт прорабу' : '')}</span></li>
              </ol>
            </div>)}
          {s.status === 'review' && (
            <div className="dpj-now is-review">
              <div className="dpj-now-h"><Ic n="clock" s={15} />Ждём клиента — отправлено {fmtAt(s.sentAt)}</div>
              <div className="dpj-now-s">Клиент видит этап в «Согласованиях» и по ссылке. Пока ждёте — можно дополнить файлами или начать следующий этап{next ? ' «' + next.short + '»' : ''}.</div>
              {next && <div className="dpj-now-a"><Button variant="secondary" size="sm" iconRight={<Ic n="arrow-right" s={13} />} onClick={() => setOpen(next.id)}>Начать «{next.short}»</Button></div>}
            </div>)}
          {finished && (
            <div className="dpj-now is-done">
              <div className="dpj-now-h"><Ic n="check-circle-2" s={15} />{s.status === 'approved' ? 'Утверждено клиентом' : 'Этап готов'}{s.approvedAt ? ' · ' + fmtAt(s.approvedAt) : ''}</div>
              {next ? <div className="dpj-now-s">Следующий этап — «{next.title}»: {next.todo}.</div>
                : coreDone ? <div className="dpj-now-s">Все этапы утверждены — финальный дизайн-проект готов: презентация для клиента и техническая часть для прораба и сметы.</div>
                : <div className="dpj-now-s">Остальные этапы уже утверждены.</div>}
              <div className="dpj-now-a">
                {next && <Button variant="accent" size="sm" iconRight={<Ic n="arrow-right" s={13} />} onClick={() => setOpen(next.id)}>Перейти к «{next.short}»</Button>}
                {coreDone && <a className="chip chip--money-strong" href={album('pres')} target="_blank" rel="noreferrer"><Ic n="presentation" s={12} />Презентация</a>}
                {coreDone && <a className="chip chip--control" href={album('tech')} target="_blank" rel="noreferrer"><Ic n="file-text" s={12} />Техчасть</a>}
              </div>
            </div>)}
          {cur === 'spec'
            ? <SpecTable dealId={dealId} rows={s.rows || []} rooms={rooms} readonly={s.status === 'approved'} />
            : cur === 'supervision'
              ? <div className="dpj-hint">Замечания по ходу стройки ведутся в «Заметках к работам» кабинета дизайнера; здесь — файлы по надзору (фото, акты осмотра).</div>
              : null}
          {cur !== 'spec' && (
            <React.Fragment>
              <Items items={s.items} readonly={s.status === 'approved'} onRemove={(id) => D.removeItem(dealId, cur, id)} />
              {s.status !== 'approved' && (
                <div className="dpj-add">
                  <Button variant="secondary" size="sm" disabled={busy} iconLeft={<Ic n="upload" s={13} />} onClick={add}>{busy ? 'Загружаю…' : (meta.accepts.includes('pdf') ? 'Картинки / PDF' : 'Картинки')}</Button>
                  {meta.accepts.includes('link') && <div className="cc-wf-linkrow"><input className="cc-ir-input" value={link} placeholder="Ссылка (Pinterest, Figma, облако)" onChange={(e) => setLink(e.target.value)} /><button type="button" className="cc-ir-btn ghost" onClick={addLink}>Добавить</button></div>}
                </div>)}
            </React.Fragment>)}
          <label className="fe-field" style={{ marginTop: 12 }}><span className="fe-label">Пояснение для клиента</span><textarea className="fe-input fe-textarea" rows={2} disabled={s.status === 'approved'} value={s.note || ''} placeholder="Что смотреть, какие варианты, на что обратить внимание" onChange={(e) => D.setNote(dealId, cur, e.target.value)} /></label>
          <Comments list={s.comments} />
          <div className="dpj-actions dpj-actions--rail">
            {meta.approver === 'client' && (s.status === 'draft' || s.status === 'changes') && <span title={canSend ? '' : 'сначала загрузите материалы'}><Button variant="accent" size="md" disabled={!canSend} iconLeft={<Ic n="send" s={14} />} onClick={() => D.send(dealId, cur)}>{s.status === 'changes' ? 'Отправить версию ' + s.version + ' клиенту' : 'Отправить клиенту на согласование'}</Button></span>}
            {canDone && <span title={nItems || meta.min === 0 ? '' : 'сначала загрузите материалы'}><Button variant="accent" size="md" disabled={!nItems && meta.min !== 0} iconLeft={<Ic n="check" s={14} />} onClick={() => D.markDone(dealId, cur)}>Этап готов</Button></span>}
            {s.status === 'review' && next && <Button variant="secondary" size="md" iconRight={<Ic n="arrow-right" s={14} />} onClick={() => setOpen(next.id)}>Начать «{next.short}»</Button>}
            {finished && next && <Button variant="accent" size="md" iconRight={<Ic n="arrow-right" s={14} />} onClick={() => setOpen(next.id)}>Перейти к «{next.short}»</Button>}
            <span style={{ flex: 1 }} />
            {finished && <Button variant="ghost" size="sm" onClick={() => D.reopen(dealId, cur)}>Новая версия</Button>}
            {s.status === 'review' && <Button variant="ghost" size="sm" title="Если клиент согласовал устно или в чате" onClick={() => { if (window.confirm('Утвердить «' + meta.title + '» от имени клиента? Обычно клиент согласует сам в кабинете.')) D.approve(dealId, cur, 'designer'); }}>Утвердить за клиента</Button>}
          </div>
        </div>
      );
    }
  }

  /* ─── клиент: согласования ─── */
  function DpClientApprovals({ dealId, who }) {
    const D = P(); useTick();
    const d = D && dealId ? D.load(dealId) : null; // только чтение: у клиента пайплайн не создаётся раньше времени
    const [txt, setTxt] = useState({});
    if (!D || !d) return <div className="lk-empty-note">Дизайнер ещё не отправлял этапы на согласование.</div>;
    // клиент видит только то, что ему отправили, и уже решённое; черновики дизайнера — нет
    const list = D.STAGES.map((s) => ({ ...s, ...d.stages[s.id] })).filter((s) => ['review', 'approved', 'changes', 'done'].indexOf(s.status) >= 0);
    if (!list.length) return <div className="lk-empty-note">Дизайнер ещё не отправлял этапы на согласование — как только отправит, они появятся здесь.</div>;
    const review = list.filter((s) => s.status === 'review'); const rest = list.filter((s) => s.status !== 'review');
    const card = (s, active) => {
      const [l, tone] = D.label(s.status);
      return (
        <div key={s.id} className={'dpj-client' + (active ? ' is-active' : '')}>
          <div className="dpj-stage-h"><div><div className="dpj-stage-t"><Ic n={s.icon} s={16} />{s.title}<span className="dpj-muted"> · v{s.version}</span></div><div className="dpj-stage-r">{s.result}</div></div><Badge tone={tone} dot>{l}</Badge></div>
          {s.note && <p className="dpj-note">{s.note}</p>}
          {s.id === 'spec' ? <SpecTable dealId={dealId} rows={s.rows || []} readonly /> : <Items items={s.items} readonly />}
          <Comments list={s.comments} />
          {active && (
            <div className="dpj-decide">
              <textarea className="fe-input fe-textarea" rows={2} value={txt[s.id] || ''} placeholder="Комментарий (обязателен, если нужны правки)" onChange={(e) => setTxt({ ...txt, [s.id]: e.target.value })} />
              <div className="dpj-actions">
                <Button variant="accent" size="md" iconLeft={<Ic n="check" s={14} />} onClick={() => { if ((txt[s.id] || '').trim()) D.addComment(dealId, s.id, { by: who || 'Клиент', role: 'client', text: txt[s.id].trim() }); D.approve(dealId, s.id, 'client'); }}>Согласовать</Button>
                <Button variant="secondary" size="md" disabled={!(txt[s.id] || '').trim()} onClick={() => D.requestChanges(dealId, s.id, (txt[s.id] || '').trim(), who || 'Клиент')}>Нужны правки</Button>
              </div>
            </div>)}
        </div>);
    };
    return (
      <div className="dpj-client-list">
        {review.length > 0 && <div className="dpj-client-h">Ждут вашего решения · {review.length}</div>}
        {review.map((s) => card(s, true))}
        {rest.length > 0 && <div className="dpj-client-h">Дизайн-проект · этапы</div>}
        {rest.map((s) => card(s, false))}
      </div>
    );
  }

  /* ─── сводка для мастера / карточки сделки ─── */
  function DpProgressCard({ dealId, href }) {
    const D = P(); usePipeline(dealId);
    const sm = D ? D.summary(dealId) : null;
    if (!sm) return null;
    return (
      <div className="dpj-prog">
        <div className="dpj-prog-h"><b>Дизайн-проект</b><span className="t-num">{sm.finished}/{sm.total} этапов · {sm.progress}%</span></div>
        <div className="dpj-prog-steps">{sm.stages.map((s) => { const [l, tone] = D.label(s.status); return <span key={s.id} className={'dpj-prog-st is-' + s.status} title={s.title + ': ' + l}>{s.short}</span>; })}</div>
        <div className="dpj-prog-l">{sm.label}{sm.specTotal ? ' · спецификация ' + rub(sm.specTotal) : ''}</div>
        {href && <a className="chip" href={href}>Открыть ДП</a>}
      </div>
    );
  }
  /* ─── этапы ДП на просмотр: менеджер (все этапы, файлы, спецификация, комментарии) и клиент (урезанно: основные этапы,
     файлы — только отправленные ему, без черновиков и цен). Раскрываются на месте, без перехода в кабинет дизайнера ─── */
  const VIEW_ST = {
    team: { approved: 'Утверждено', done: 'Готово', review: 'У клиента на согласовании', changes: 'Правки — у дизайнера', draft: 'В работе у дизайнера', pending: 'Не начат' },
    client: { approved: 'Согласовано', done: 'Готово', review: 'Ждёт вашего решения', changes: 'Дизайнер вносит правки', draft: 'Дизайнер работает', pending: 'Впереди' },
  };
  function DpStagesView({ dealId, who }) {
    const D = P(); useTick();
    const [open, setOpen] = useState(undefined);
    const d = D && dealId ? D.load(dealId) : null; // только чтение: пайплайн не создаётся с этого экрана
    if (!D || !d) return <div className="dpx-lock">Дизайнер ещё не начал — этапы появятся здесь.</div>;
    const client = who === 'client';
    const sm = D.summary(dealId);
    const list = client ? D.CORE : D.STAGES;
    const words = client ? VIEW_ST.client : VIEW_ST.team;
    const openId = open !== undefined ? open : client ? ((sm.review[0] || {}).id || null) : (sm.current ? sm.current.id : null);
    const shown = (x) => ['review', 'approved', 'done', 'changes'].indexOf(x.status) >= 0; // клиенту — только то, что ему отправляли
    const body = (st, x) => {
      if (x.status === 'pending') return <div className="dpx-lock">{client ? 'Дизайнер возьмётся после предыдущих этапов.' : 'Этап ещё не начат.'}</div>;
      return (
        <React.Fragment>
          <div className="dsv-res"><Ic n={st.icon} s={14} /><span>{st.result}</span></div>
          {x.note && (!client || shown(x)) && <p className="dpj-note">{x.note}</p>}
          {st.id === 'spec'
            ? (client ? <div className="dsv-muted">{(x.rows || []).length && shown(x) ? x.rows.length + ' поз. — полный список в «Согласованиях»' : 'Дизайнер собирает список материалов и мебели.'}</div>
              : <SpecTable dealId={dealId} rows={x.rows || []} readonly />)
            : (!client || shown(x)) ? <Items items={x.items || []} readonly />
              : <div className="dsv-muted">Дизайнер работает — покажет, когда отправит на согласование.</div>}
          {!client && <Comments list={x.comments} />}
          {client && x.status === 'review' && <div className="rail-acts"><a className="dpx-btn accent" href={'client-approvals.html?deal=' + encodeURIComponent(dealId)}>Согласовать или попросить правки<Ic n="arrow-right" s={12} /></a></div>}
        </React.Fragment>
      );
    };
    return (
      <div className="rail rail--sub dsv">
        {list.map((st, i) => {
          const x = d.stages[st.id] || {}; const done = x.status === 'approved' || x.status === 'done'; const isOpen = openId === st.id;
          const nFiles = (x.items || []).length; const nRows = (x.rows || []).length;
          const count = st.id === 'spec' ? (nRows ? nRows + ' поз.' : '') : (nFiles ? nFiles + ' файл.' : '');
          const when = done && x.approvedAt ? ' · ' + fmtAt(x.approvedAt) : x.status === 'review' && x.sentAt && !client ? ' · с ' + fmtAt(x.sentAt) : '';
          return (
            <div key={st.id} className={'rail-row ' + (done ? 'is-done' : x.status === 'pending' ? 'is-todo' : 'is-cur') + (isOpen ? ' is-open' : '') + (client && x.status === 'review' ? ' is-ask' : '')}>
              <div className="rail-line"><span className="rail-node">{done ? <Ic n="check" s={12} /> : i + 1}</span><span className="rail-stem" /></div>
              <div className="rail-body">
                <button type="button" className="rail-head" onClick={() => setOpen(isOpen ? null : st.id)} aria-expanded={isOpen}>
                  <span className="rail-t">{st.title}</span>
                  {!isOpen && count && (!client || shown(x)) ? <span className="rail-meta">{count}</span> : null}
                  <span className="rail-right">{(words[x.status] || x.status) + when}</span>
                  <span className="rail-chev"><Ic n={isOpen ? 'chevron-up' : 'chevron-down'} s={13} /></span>
                </button>
                {isOpen && <div className="rail-panel dsv-panel">{body(st, x)}</div>}
              </div>
            </div>);
        })}
      </div>
    );
  }

  /* ─── сквозной процесс ДП: бриф → выбор дизайнера → разработка → финальный ДП → состав работ. Сразу видно: где стоим, на ком, что дальше ─── */
  const WHO_ICON = { client: 'user-round', designer: 'pen-tool', manager: 'briefcase' };
  const STEP5_FULL = ['бриф', 'выбор дизайнера', 'разработка дизайн-проекта', 'финальный дизайн-проект', 'состав работ и смета'];
  const STEP5_SHORT = ['бриф', 'дизайнер', 'разработка ДП', 'финальный ДП', 'состав работ'];
  /* дайджест брифа для шага «Выбор дизайнера»: стиль (лайкнутые карточки из dp_brief.responses.styleVotes) +
     уровень отделки (тот же словарь, что в блоке данных сделки, lk/deal-data-block.jsx DICT/DealDict) —
     минимум, с чем идти выбирать дизайнера; частей, которых нет в данных, просто не показываем */
  function briefDigest(dealId, intake) {
    const it = intake || (window.loadSiteIntake && window.loadSiteIntake(dealId)) || {};
    const parts = [];
    try {
      const F = window.FormInstances;
      const inst = F && F.findInstance ? F.findInstance(dealId, (i) => i.kind === 'dp_brief') : null;
      const votes = inst && inst.responses ? inst.responses.styleVotes : null;
      const DP = window.DpBriefTemplateMock;
      if (votes && DP && DP.STYLE_CARDS) {
        const liked = DP.STYLE_CARDS.filter((c) => votes[c.id] === 'like').map((c) => c.title);
        if (liked.length) parts.push('стиль — ' + liked.join(', '));
      }
    } catch (e) {}
    const tier = ((it.prefs) || {}).finishTier;
    const DD = window.DealDict;
    const tierLabel = tier && DD && DD.tier ? (DD.tier.find((x) => x[0] === tier) || [])[1] : null;
    if (tierLabel) parts.push('уровень — ' + tierLabel);
    return parts.length ? 'По брифу: ' + parts.join(', ') : '';
  }
  function DpProcessCard({ dealId, intake, onBrief, hrefBrief, onWorks, hrefWorks, compact, title, who, stagesSlot, summaryRow }) {
    const D = P(); const tick = useTick();
    const [openStep, setOpenStep] = useState(null); // null — раскрыт текущий шаг; клик по заголовку раскрывает любой
    const [pickDesigner, setPickDesigner] = useState('');
    const pr = useMemo(() => (D && D.process && dealId ? D.process(dealId, intake) : null), [D, dealId, intake, tick]);
    if (!pr || !pr.requiresDesign) return null;
    const { brief, dev, final, works, step } = pr;
    const album = (tab) => 'dp-album.html?deal=' + encodeURIComponent(dealId) + (tab ? '&tab=' + tab : '');
    // дизайнер, открывший карточку сделки, может сам взять в работу шаг «Выбор дизайнера», если за сделкой ещё не закреплён дизайнер
    const me = window.AccessStore && window.AccessStore.currentAccount ? window.AccessStore.currentAccount() : null;
    const isDesignerAcct = !!me && (me.role === 'designer' || (me.role === 'executor' && me.trade === 'designer'));
    const claimDesigner = () => { const T = window.DealTeam; if (!T || !me) return; T.assign(dealId, 'designer', { id: me.profileId, name: me.name, how: 'взял в работу' }); };
    const designerOptions = (window.DealTeam && window.DealTeam.options ? window.DealTeam.options('designer') : []) || [];
    const assignDesigner = () => { const T = window.DealTeam; if (!T || !pickDesigner) return; const opt = designerOptions.find((o) => o.name === pickDesigner);
      // аккаунтов дизайнеров нет — назначаем по имени (как «Назначить» в «Команде» сделки)
      const p = opt ? { id: opt.id, name: opt.name } : (!designerOptions.length && pickDesigner.trim() ? { id: null, name: pickDesigner.trim() } : null);
      if (p) T.assign(dealId, 'designer', { ...p, how: 'выбран по брифу' }); setPickDesigner(''); };
    const digest = dev.needDesigner ? briefDigest(dealId, intake) : '';
    // этапы раскрываются здесь же; в рабочий кабинет этапов ведёт только ссылка дизайнера, клиенту — «Согласования», когда этап ждёт его
    const designerHref = 'designer-design.html?deal=' + encodeURIComponent(dealId);
    const approvalsHref = 'client-approvals.html?deal=' + encodeURIComponent(dealId);
    const nReview = dev.summary ? dev.summary.review.length : 0;
    const coreSt = (id) => (dev.summary ? (dev.summary.stages.find((y) => y.id === id) || {}).status : 'pending');
    const inWork = D.CORE.filter((s) => { const x = coreSt(s.id); return x && x !== 'pending' && x !== 'approved' && x !== 'done'; }).length;
    const ahead = Math.max(0, dev.coreTotal - dev.coreDone - inWork);
    // «Разработка ДП» (шаг process().step) делится на 5 экранных шагов: до брифа шаг всегда 0; после брифа,
    // пока не выбран дизайнер, «шаг 1» process() показываем как отдельный шаг «Выбор дизайнера»
    const displayIdx = step === 0 ? 0 : (dev.needDesigner ? 1 : step + 1);
    const isOpen = (i) => (openStep == null ? i === displayIdx : openStep === i);
    const Who = ({ who, label, next }) => (who ? <div className={'dpx-who is-' + who}><Ic n={WHO_ICON[who] || 'user-round'} s={13} /><b>{label}</b><span>{next}</span></div> : null);
    const STAGE_ST = { approved: ['Утверждено', 'done'], done: ['Готово', 'done'], review: ['На согласовании у клиента', 'cur'], changes: ['Клиент просит правки', 'cur'], draft: ['В работе у дизайнера', 'cur'], pending: ['Не начат', ''] };
    const rowCls = (i) => (i < displayIdx ? 'is-done' : i === displayIdx ? 'is-cur' : 'is-todo');
    const act = (node) => (node ? <div className="rail-acts">{node}</div> : null);
    const steps = [
      { n: 1, t: 'Бриф на дизайн-проект', hint: 'анкета клиента', right: brief.status === 'approved' ? 'Согласован' + (brief.approvedAt ? ' · ' + fmtAt(brief.approvedAt) : '') : brief.label,
        panel: (
          <React.Fragment>
            <div className="rail-prog"><span className="rail-prog-bar"><i style={{ width: brief.progress + '%' }} /></span><span className="rail-prog-n t-num">{brief.progress}%</span></div>
            {brief.who && <div className={'dpx-who is-' + brief.who}><Ic n={WHO_ICON[brief.who] || 'user-round'} s={13} /><b>{brief.whoLabel}</b><span>{brief.next}</span></div>}
            {act(onBrief ? <button type="button" className="dpx-btn accent" onClick={onBrief}>{brief.status === 'submitted' ? 'Проверить и согласовать' : brief.status === 'approved' ? 'Открыть бриф' : 'Открыть бриф'}</button> : hrefBrief ? <a className="dpx-btn accent" href={hrefBrief}>Открыть бриф</a> : null)}
          </React.Fragment>) },
      { n: 2, t: 'Выбор дизайнера', hint: 'дизайнер под бриф и уровень', right: dev.designer ? dev.designer.name + ' · выбран' : brief.status !== 'approved' ? 'после брифа' : 'выбрать',
        panel: dev.designer
          ? <Who who="designer" label={dev.designer.name} next={dev.designer.how || 'назначен'} />
          : dev.needDesigner
            ? (
              <React.Fragment>
                {digest && <div className="dpx-brief-digest">{digest}</div>}
                {who === 'designer' && isDesignerAcct
                  ? act(<button type="button" className="dpx-btn accent" onClick={claimDesigner}>Взять в работу</button>)
                  : who === 'client'
                    ? <div className="dpx-lock">Подбираем дизайнера под ваш бриф</div>
                    : act(
                        <span className="dpx-acts dpx-acts--pick">
                          {designerOptions.length
                            ? <select className="dpx-select" value={pickDesigner} onChange={(e) => setPickDesigner(e.target.value)}>
                                <option value="">Выбрать дизайнера…</option>
                                {designerOptions.map((o) => <option key={o.name} value={o.name}>{o.name}{o.sub ? ' · ' + o.sub : ''}</option>)}
                              </select>
                            : <input className="dpx-select" placeholder="Имя дизайнера" value={pickDesigner} onChange={(e) => setPickDesigner(e.target.value)} />}
                          <button type="button" className="dpx-btn accent" disabled={!pickDesigner.trim()} onClick={assignDesigner}>Назначить</button>
                        </span>)}
              </React.Fragment>)
            : <div className="dpx-lock">откроется, когда бриф согласован</div> },
      { n: 3, t: 'Разработка дизайн-проекта', hint: 'концепция → чертежи → спецификация', right: dev.active ? dev.coreDone + ' из ' + dev.coreTotal + ' этапов' + (compact ? '' : ' · ' + Math.round((dev.coreDone / dev.coreTotal) * 100) + '%') : dev.needDesigner ? 'не начата' : 'после брифа',
        panel: dev.active ? (
          <React.Fragment>
            {!stagesSlot && dev.who && <Who who={dev.who} label={dev.whoLabel} next={dev.next} />}
            <div className="dpx-slot-h"><b>Этапы</b><span>пройдено {dev.coreDone} · в работе {inWork} · впереди {ahead}</span></div>
            {stagesSlot || <DpStagesView dealId={dealId} who={who} />}
            {!stagesSlot && who === 'client' && nReview > 0 && act(<a className="dpx-btn accent" href={approvalsHref}>Согласовать · {nReview}<Ic n="arrow-right" s={12} /></a>)}
            {!stagesSlot && who === 'designer' && act(<a className="dpx-btn" href={designerHref}>Открыть в работе<Ic n="arrow-up-right" s={12} /></a>)}
          </React.Fragment>) : dev.needDesigner
          ? <div className="dpx-lock">Разработка начнётся после выбора дизайнера</div>
          : <div className="dpx-lock">откроется, когда бриф согласован</div> },
      { n: 4, t: 'Финальный дизайн-проект', hint: 'презентация + техчасть', right: final.ready ? 'Готов' : dev.active ? 'осталось ' + final.missing.length : 'после разработки',
        panel: final.ready ? (
          <React.Fragment>
            <div className="rail-items">
              <div className="rail-item is-done"><span className="rail-item-ic"><Ic n="check" s={11} /></span><span className="rail-item-t">Визуализации и планы</span><span className="rail-item-r">{final.viz} файл.</span></div>
              <div className="rail-item is-done"><span className="rail-item-ic"><Ic n="check" s={11} /></span><span className="rail-item-t">Чертежи</span><span className="rail-item-r">{final.docs} файл.</span></div>
              <div className="rail-item is-done"><span className="rail-item-ic"><Ic n="check" s={11} /></span><span className="rail-item-t">Спецификация</span><span className="rail-item-r">{final.specRows} поз. · {rub(final.specTotal)}</span></div>
            </div>
            {act(<span className="dpx-acts"><a className="dpx-btn accent" href={album('pres')} target="_blank" rel="noreferrer"><Ic n="presentation" s={12} />Презентация</a><a className="dpx-btn" href={album('tech')} target="_blank" rel="noreferrer"><Ic n="file-text" s={12} />Техчасть</a><a className="dpx-btn" href={album('all') + '&print=1'} target="_blank" rel="noreferrer"><Ic n="file-down" s={12} />PDF</a></span>)}
          </React.Fragment>) : <div className="dpx-lock">{dev.active ? 'появится после утверждения: ' + final.missing.join(', ') : 'после разработки'}</div> },
      { n: 5, t: 'Состав работ и смета', hint: 'спецификация → материалы, чертежи → работы', right: !works.ready ? 'после чертежей и спецификации' : works.estStarted ? (works.estStatus === 'draft' ? 'смета собирается' : works.estStatus === 'client_review' ? 'у клиента' : works.estStatus === 'ready' ? 'согласована' : works.estStatus) : 'можно собирать',
        panel: works.ready ? (
          <React.Fragment>
            <div className="rail-items">
              <div className="rail-item is-done"><span className="rail-item-ic"><Ic n="check" s={11} /></span><span className="rail-item-t">Спецификация → чистовые материалы</span><span className="rail-item-r">{works.specRows} поз. · {rub(works.specTotal)}</span></div>
              <div className="rail-item is-done"><span className="rail-item-ic"><Ic n="check" s={11} /></span><span className="rail-item-t">Чертежи → состав работ</span><span className="rail-item-r">{works.docs} файл.</span></div>
              <div className={'rail-item' + (works.estStarted ? ' is-cur' : '')}><span className="rail-item-ic" /><span className="rail-item-t">Смета по каталогу</span><span className="rail-item-r">{works.estTotal ? rub(works.estTotal) : 'собирается'}</span></div>
            </div>
            {act(onWorks ? <button type="button" className="dpx-btn accent" onClick={onWorks}>К составу работ<Ic n="arrow-right" s={12} /></button> : hrefWorks ? <a className="dpx-btn accent" href={hrefWorks}>К составу работ<Ic n="arrow-right" s={12} /></a> : null)}
          </React.Fragment>) : <div className="dpx-lock">{dev.active ? 'откроется, когда утверждены ' + (works.missing || []).map((x) => ({ docs: 'чертежи', spec: 'спецификация' }[x] || x)).join(' и ') : 'после разработки ДП'}</div> },
    ];
    return (
      <div className={'dpx' + (compact ? ' dpx--compact' : '')}>
        {title !== false && <div className="dpx-h"><b>{title || 'Процесс дизайн-проекта'}</b>{summaryRow === false && <span>шаг {displayIdx + 1} из 5 · {STEP5_SHORT[displayIdx]}</span>}</div>}
        {summaryRow !== false && (
          <div className="dpx-sum">
            <span><b className="dpx-sum-n">Шаг {displayIdx + 1} из 5</b> <span className="dpx-sum-s">· {STEP5_FULL[displayIdx]}</span></span>
            <span className="dpx-sum-bar"><i style={{ width: (dev.active ? Math.round((dev.coreDone / dev.coreTotal) * 100) : brief.progress) + '%' }} /></span>
            <span>{dev.active
              ? <React.Fragment><b className="dpx-sum-n">{dev.coreDone} из {dev.coreTotal} этапов</b> <span className="dpx-sum-s">· осталось {dev.coreTotal - dev.coreDone}{dev.summary && dev.summary.current ? ' · сейчас «' + dev.summary.current.short + '»' : ''}</span></React.Fragment>
              : dev.needDesigner
                ? <React.Fragment><b className="dpx-sum-n">Бриф согласован</b> <span className="dpx-sum-s">· выберите дизайнера</span></React.Fragment>
                : <React.Fragment><b className="dpx-sum-n">Бриф {brief.progress}%</b> <span className="dpx-sum-s">· этапы ДП после согласования</span></React.Fragment>}</span>
          </div>)}
        <div className="rail dpx-rail">
          {steps.map((t, i) => (
            <div key={t.n} className={'rail-row ' + rowCls(i)}>
              <div className="rail-line"><span className="rail-node">{i < displayIdx ? <Ic n="check" s={14} /> : t.n}</span><span className="rail-stem" /></div>
              <div className="rail-body">
                <button type="button" className="rail-head" onClick={() => setOpenStep(isOpen(i) ? -1 : i)} aria-expanded={isOpen(i)}>
                  <span className="rail-t">{t.t}</span>
                  {!isOpen(i) && t.hint && <span className="rail-meta">{t.hint}</span>}
                  <span className="rail-right">{t.right}</span>
                  <span className="rail-chev"><Ic n={isOpen(i) ? 'chevron-up' : 'chevron-down'} s={13} /></span>
                </button>
                {isOpen(i) && <div className="rail-panel">{t.panel}</div>}
              </div>
            </div>))}
        </div>
      </div>
    );
  }
  /* ─── альбом: итоговый вид; only — какие этапы (презентация / техчасть) ─── */
  function DpAlbum({ dealId, only, emptyText }) {
    const D = P(); usePipeline(dealId);
    const a0 = D ? D.album(dealId) : null;
    const a = a0 && only ? { ...a0, stages: a0.stages.filter((s) => only.indexOf(s.id) >= 0) } : a0;
    if (!a || !a.stages.length) return <div className="lk-empty-note">{emptyText || 'Утверждённых этапов пока нет.'}</div>;
    return (
      <div className="dpj-album">
        {a.stages.map((s) => (
          <section key={s.id} className="dpj-album-s">
            <h3><Ic n={s.icon} s={15} />{s.title}<span className="dpj-muted"> · утверждено {fmtAt(s.approvedAt)}</span></h3>
            {s.note && <p className="dpj-note">{s.note}</p>}
            {s.id === 'spec' ? <SpecTable dealId={dealId} rows={a.spec} readonly /> : <Items items={s.items} readonly />}
          </section>))}
      </div>
    );
  }
  Object.assign(window, { DpStagesPanel, DpStagesView, DpClientApprovals, DpProgressCard, DpAlbum, DpProcessCard });
})();
