/* client-v2-prestart.jsx — новый кабинет клиента до запуска проекта (спека 2026-10-26-client-v2-prestart, «Как выглядит»).
   Проекта ещё нет, поэтому всё берётся из сделки (ClientDealViews, plain JS, одни правила шагов, главного действия и меню):
   главная «где вы сейчас» с одним главным действием, шаги пути своей ветки («сделано / сейчас / дальше»), меню по правилам
   (Смета · Документы · Оплаты · Дизайн · Чат) и окна кабинета до запуска. После запуска та же ссылка открывает обычную главную
   (client-v2-app.jsx), шагов «до старта» там нет.
   Грузится после client-v2-app.jsx и дополняет window.CV: Pre = { PreHome, PreSteps, PreApp, PreWindow, Frame, windows }.
   Окна (смета/КП, анкеты, документы/оплаты, чат) подключаются в Pre.windows[id] = (props) => JSX с рамкой Pre.Frame; анкеты — «Замер»
   и «Бриф» (логика форм — client-intake-forms.jsx, оформление — client-v2.css .cv-forms); «Документы», «Оплаты» и «Чат» — по сделке
   (ClientDealViews.docs/money/chat; карточки — CV.DocCard, переписка — CV.ChatConv через DealChat);
   пока окна нет, показывается заглушка в той же рамке v2. Все действия клиента — через хранилища из RO_SPEC (client-v2-app.jsx):
   у сотрудников они «только чтение». */
(function () {
  'use strict';
  const CV = window.CV;
  if (!CV) return;
  const { Ic, Header, TeamBlock, DocCard, ChatConv, P, greeting, todayLine, designHref, plural, rub } = CV;
  const V = () => window.ClientDealViews;

  // подписи шагов пути; у отменённого дизайн-проекта — «отменён» (пометка «дизайн-проект отменён» приходит из ClientDealViews)
  const STATE_L = { done: 'сделано', current: 'сейчас', next: 'дальше', cancelled: 'отменён' };
  const TITLES = { measure: 'Замер', brief: 'Бриф на дизайн-проект', estimate: 'Смета', docs: 'Документы', payments: 'Оплаты', chat: 'Чат' };

  /* шаги пути своей ветки: ветка с дизайн-проектом — 9 шагов, без него — 5 (ClientDealViews.path) */
  function PreSteps({ path }) {
    if (!path || !path.steps.length) return null;
    return (
      <section className="cv-steps" aria-labelledby="cv-steps-h">
        <h2 className="h2" id="cv-steps-h">Путь вашего ремонта</h2>
        <div className="lab" style={{ marginTop: 6 }}>{path.design ? 'С дизайн-проектом' : 'Без дизайн-проекта'}</div>
        <ol>
          {path.steps.map((s, i) => (
            <li key={s.id} className={'is-' + s.state} aria-current={s.state === 'current' ? 'step' : undefined}>
              <span className="n" aria-hidden="true">{s.state === 'done' ? <Ic n="check" s={14} c="currentColor" /> : i + 1}</span>
              <span className="t">{s.title}{s.note ? <span className="lab"> · {s.note}</span> : null}</span>
              <span className="st">{STATE_L[s.state]}</span>
            </li>))}
        </ol>
      </section>);
  }

  /* главная до запуска: «где вы сейчас» + одно главное действие, шаги, команда */
  function PreHome({ dealId, open }) {
    const v = V();
    const h = v.home(dealId);
    const act = h.action;
    const first = (h.clientName || '').split(' ')[0];
    const go = () => { if (act.window === 'design') window.location.href = designHref('stages'); else open(act.window); };
    // «субъект» сделки: команда (P.views.clientTeam) и визитка менеджера в шапке берутся из назначений сделки
    const subject = v.subject(dealId);
    return (
      <div className="cv-page">
        <Header name={h.clientName} open={open} design={h.path.design} menu={v.menu(dealId)} project={subject} />
        <main id="main" tabIndex={-1} className="cv-main">
          <section className="cv-hero">
            <div>
              <div className="lab">{todayLine()}{h.address ? ' · ' + h.address : ''}</div>
              <h1 className="cv-hello">{greeting()}{first ? ', ' + first : ''}</h1>
              <p className="cv-where"><span className="lab">Где вы сейчас</span><b>{h.now.title}</b>{act ? null : <span>{h.now.text}</span>}</p>
            </div>
            <div className="cv-hero-act">
              {act
                ? <button type="button" className="btn-k" onClick={go}>{act.label}</button>
                : <span className="pill-g"><span className="dot" />От вас сейчас ничего не нужно</span>}
            </div>
          </section>

          <PreSteps path={h.path} />

          <TeamBlock t={P.views.clientTeam(subject)} open={open} />
        </main>
      </div>);
  }

  /* рамка окна кабинета — как у остальных окон v2 (скрим, шапка, «Закрыть», Esc) */
  function Frame({ title, lab, onClose, children, cls }) {
    React.useEffect(() => { const k = (e) => e.key === 'Escape' && onClose(); document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k); }, []);
    /* окно КП печатается без остальной страницы: класс на body нужен print-CSS только пока оно открыто */
    const kp = !!cls && cls.indexOf('cv-kpmod') >= 0;
    React.useEffect(() => {
      const cl = kp && document.body && document.body.classList;
      if (!cl) return undefined;
      cl.add('cv-print-kp');
      return () => cl.remove('cv-print-kp');
    }, [kp]);
    return (
      <React.Fragment>
        <div className="cv-scrim" onClick={onClose} />
        <div className={'cv-modal' + (cls ? ' ' + cls : '')} role="dialog" aria-modal="true" aria-label={title}>
          <div className="cv-mhead">
            <div>{lab ? <div className="lab">{lab}</div> : null}<h1 className="h1" style={{ marginTop: lab ? 10 : 0 }}>{title}</h1></div>
            <button type="button" className="x" aria-label="Закрыть" onClick={onClose}><Ic n="x" /></button>
          </div>
          {children}
        </div>
      </React.Fragment>);
  }

  const windows = {};

  /* ── окна анкет: «Замер» и «Бриф» (спека, «Уточнения после проверки кода»: формы) ──
     Анкеты — те же компоненты, что в старом кабинете (window.ClientIntakeForms), но в стиле client-v2.css и без старой дизайн-системы.
     Сотрудники («ЛК от имени», руководитель) видят окна «только чтение»: поля отключены, отправки и кнопки на бриф нет */
  const MONTHS = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
  const fmtDate = (d) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(d || ''); return m ? (+m[3]) + ' ' + MONTHS[+m[2] - 1] : (d || ''); };
  const isRO = () => !!(CV.ACCESS && CV.ACCESS.readOnly);
  /* журнал действий клиента до старта (lk/deal-events.js, KSH-491): сотрудник в «ЛК от имени» событий не пишет */
  const track = (dealId, type, params) => { try { if (!isRO() && window.DealEvents) window.DealEvents.log(dealId, type, params); } catch (e) { /* журнал не ломает кабинет */ } };
  /* часов с отправки сметы клиенту (est.offer.sentAt, EstimateOffer.stamp); нет даты — без параметра */
  const hoursSinceSent = (dealId) => {
    try { const e = JSON.parse(window.localStorage.getItem('calc2_estimate_v2_' + dealId) || 'null'); const t = Date.parse(e && e.offer && e.offer.sentAt);
      return isFinite(t) ? Math.max(0, Math.round((Date.now() - t) / 36e5)) : null; } catch (e) { return null; }
  };
  const RoNote = ({ text }) => <div className="note-s" role="note" style={{ marginTop: 20 }}>{text || 'Режим «только чтение»: ответы за клиента отсюда не отправляются.'}</div>;
  /* формы грузятся отдельным скриптом после оболочки: если ссылка анкеты открылась раньше, окно перерисуется по его событию */
  function useForms() {
    const [, tick] = React.useState(0);
    React.useEffect(() => {
      if (window.ClientIntakeForms) return undefined;
      const on = () => tick((x) => x + 1);
      window.addEventListener('remontpro:client-intake-ready', on);
      return () => window.removeEventListener('remontpro:client-intake-ready', on);
    }, []);
    return window.ClientIntakeForms || null;
  }
  const FormsLoading = () => <div className="empty" style={{ marginTop: 28 }}>Анкета загружается…</div>;

  function MeasureWindow({ dealId, onClose }) {
    const [, tick] = React.useState(0);
    const Forms = useForms();
    const m = V().measure(dealId);
    const ro = isRO();
    const S = window.ClientIntakeSync;
    const when = [fmtDate(m.date), m.time].filter(Boolean).join(', ');
    const facts = [['Дата', when], ['Кто приедет', m.who], ['Подъезд', m.entrance], ['Код двери', m.doorCode]].filter((f) => f[1]);
    return (
      <Frame title="Замер" lab="Подготовка к замеру" onClose={onClose}>
        <section className="soft cv-mcard">
          {m.done
            ? <span className="pill-k"><span className="dot" />замер выполнен</span>
            : <span className="pill-g"><span className="dot" />{m.date ? 'замер назначен' : 'дата уточняется'}</span>}
          {m.done && m.area ? <p className="cv-mtext">Площадь по замеру — <b className="num">{m.area} м²</b></p> : null}
          {facts.length ? <dl className="cv-mfacts">{facts.map((f) => <div key={f[0]}><dt className="lab">{f[0]}</dt><dd><b>{f[1]}</b></dd></div>)}</dl> : null}
          {!m.done && m.date ? <p className="cv-mtext">Пожалуйста, убедитесь, что в это время на объект можно попасть.</p> : null}
        </section>
        {m.formOpen
          ? <div className="cv-forms">
            {ro ? <RoNote /> : null}
            {Forms && S ? <Forms.MeasurePrepForm dealId={dealId} intake={S.loadIntake(dealId)} readOnly={ro} onSaved={() => tick((x) => x + 1)} /> : <FormsLoading />}
          </div>
          : null}
      </Frame>);
  }

  function BriefWindow({ dealId, onClose }) {
    const [, tick] = React.useState(0);
    const Forms = useForms();
    const b = V().brief(dealId);
    const ro = isRO();
    const S = window.ClientIntakeSync;
    return (
      <Frame title="Бриф" lab="Дизайн-проект" onClose={onClose}>
        {b.link || b.status === 'approved' || b.status === 'expired'
          /* бриф выдан по ссылке: форма живёт на brief.html (form-engine), здесь — статус и переход */
          ? <section className="soft cv-mcard">
            <span className={b.status === 'approved' ? 'pill-k' : b.status === 'expired' ? 'pill-d' : 'pill-g'}><span className="dot" />{b.label}</span>
            <p className="cv-mtext">{b.status === 'approved'
              ? 'Дизайнер согласовал бриф — дальше концепция.'
              : b.status === 'expired' ? 'Менеджер пришлёт новую ссылку на бриф.'
              : 'Подробная анкета для дизайнера: семья и быт, стиль, референсы, планировка, кухня, инженерия, мебель, бюджет. Заполняйте частями — сохраняется само.'}</p>
            {b.link && b.status !== 'approved' && !ro
              ? <a className={b.status === 'submitted' ? 'btn-l' : 'btn-k'} href={b.link}>{b.status === 'submitted' ? 'Посмотреть ответы' : 'Заполнить бриф'}</a>
              : null}
          </section>
          : null}
        {ro ? <RoNote /> : null}
        {!b.link && b.status !== 'approved' && b.status !== 'expired'
          ? <div className="cv-forms">{Forms && S ? <Forms.DpPrefsForm dealId={dealId} intake={S.loadIntake(dealId)} readOnly={ro} onSaved={() => tick((x) => x + 1)} /> : <FormsLoading />}</div>
          : null}
      </Frame>);
  }
  windows.measure = MeasureWindow;
  windows.brief = BriefWindow;

  /* ── окна «Документы», «Оплаты» и «Чат» (спека, «Путь клиента»): данные — из сделки (ClientDealViews), действия клиента — через
     хранилища из RO_SPEC, у сотрудников («ЛК от имени») окна «только чтение»: кнопок согласия и поля сообщения нет ── */
  /* перерисовка окна по событиям хранилища сделки (менеджер отметил оплату, написал в чат — клиент видит сразу) */
  function useDealTick(events) {
    const [, tick] = React.useState(0);
    React.useEffect(() => {
      const up = () => tick((x) => x + 1);
      events.forEach((e) => window.addEventListener(e, up));
      return () => events.forEach((e) => window.removeEventListener(e, up));
    }, []);
  }

  /* аванс подряда считает движок сметы — на странице клиента он грузится лениво. Пока движка нет, сумма неизвестна (0): окна
     «Документы» и «Оплаты» просят его сами и перерисовываются, когда он загрузился */
  /* один запрос движка сметы на страницу: окно «Документы» просит его и для оплат, и для блока договора */
  let calcP = null;
  const loadCalc = () => calcP || (calcP = window.LKLazy.calc());
  function useCalcReady(dealId) {
    const [, tick] = React.useState(0);
    const need = V().money(dealId).rows.some((r) => r.id === 'work_contract:advance' && !r.amount);
    React.useEffect(() => {
      if (!need || window.Calc2 || !(window.LKLazy && window.LKLazy.calc)) return undefined;
      let live = true;
      loadCalc().then(() => { if (live) tick((x) => x + 1); }).catch(() => {});
      return () => { live = false; };
    }, [need]);
  }

  /* договор подряда простым языком (KSH-498): «Главное в 7 пунктах», «Куда идут 30%», «Первые 14 дней после подписи».
     Модель — ClientContract.view (lk/client-contract.js): цифры из ClientEstimate.view и плана платежей, график и роли — из сметы;
     окно ничего не считает. Блок есть, когда договор подряда отправлен клиенту */
  /* движок сметы нужен блокам договора всегда (итог, план платежей, график) — подгружаем лениво, как окно сметы */
  function useCalc() {
    const [, tick] = React.useState(0);
    React.useEffect(() => {
      if (window.Calc2 || !(window.LKLazy && window.LKLazy.calc)) return undefined;
      let live = true;
      loadCalc().then(() => { if (live) tick((x) => x + 1); }).catch(() => {});
      return () => { live = false; };
    }, []);
  }
  function ContractBrief({ dealId }) {
    useCalc();
    const CC = window.ClientContract;
    const k = CC ? CC.view(dealId) : null;
    if (!k) return null;
    const a = k.advance;
    return (
      <section className="cv-ct" aria-labelledby="cv-ct-h">
        <h2 className="h2" id="cv-ct-h" style={{ fontSize: 20 }}>Договор подряда: главное в 7 пунктах</h2>
        <div className="lab" style={{ marginTop: 6 }}>Коротко и простым языком. Юридически действует текст договора.</div>
        <ol className="cv-ct-pts">
          {k.points.map((x) => <li key={x.title}><b>{x.title}</b><p>{x.text}</p></li>)}
        </ol>

        <div className="cv-ct-adv">
          <div>
            <div className="lab">Куда идут {a.pct}%</div>
            <div className="stat num" style={{ marginTop: 8 }}>{a.amount != null ? rub(a.amount) : 'по смете'}</div>
          </div>
          <ul>
            <li>Аванс засчитывается в оплату работ: счета за {a.start && a.start.kind === 'stage' ? 'этапы' : 'фазы'} уже уменьшены на него.</li>
            {a.start ? <li>Работы на объекте начинаются с этапа «{a.start.name}».</li> : null}
            {a.materials > 0 ? <li>Материалы — отдельным счётом по спецификации, {rub(a.materials)}.</li> : null}
          </ul>
        </div>

        {k.first14.length > 0 ? (
          <div className="cv-ct-14">
            <h3 className="h2" style={{ fontSize: 18 }}>Первые 14 дней после старта</h3>
            <div className="lab" style={{ marginTop: 6 }}>{k.foreman ? 'Прораб ' + k.foreman + ' ведёт объект с первого дня' : 'Прораб ведёт объект с первого дня'}. Вопросы — в чат кабинета.</div>
            <div className="cv-ct-days" aria-hidden="true">{Array.from({ length: 14 }, (_, i) => <span key={i}>{i + 1}</span>)}</div>
            <ul className="cv-ct-rows">
              {k.first14.map((r) => (
                <li key={r.name + r.from}>
                  <div className="hd"><b>{r.name}</b><span className="lab">{r.from === r.to ? 'день ' + r.from : 'дни ' + r.from + '–' + r.to}{r.more ? ' и дальше' : ''}</span></div>
                  <div className="cv-ct-bar"><i style={{ left: ((r.from - 1) / 14 * 100) + '%', width: ((r.to - r.from + 1) / 14 * 100) + '%' }} className={r.more ? 'more' : ''} /></div>
                  {r.roles.length ? <div className="lab">придут: {r.roles.join(', ').toLowerCase()}</div> : null}
                </li>))}
            </ul>
          </div>) : null}
      </section>);
  }

  /* «Добро пожаловать в команду» (KSH-498): после аванса, подтверждённого бухгалтером (ClientContract.welcome) — один раз; закрыл — отметка
     в браузере клиента. У сотрудника («ЛК от имени») экран сам не открывается и отметку не пишет. Проект запущен — кнопка ведёт в кабинет ремонта */
  function WelcomeWindow({ dealId, onClose }) {
    useCalc();
    const CC = window.ClientContract;
    const w = CC ? CC.welcome(dealId) : null;
    if (!w || !w.due) return null;
    const people = [w.manager && Object.assign({ role: 'Менеджер проекта' }, w.manager), w.foreman && Object.assign({ role: 'Прораб' }, w.foreman)].filter(Boolean);
    const close = () => { if (!isRO()) CC.markWelcomeSeen(dealId); onClose(); };
    return (
      <Frame title="Добро пожаловать в команду" lab="аванс получен — ремонт начинается" onClose={close} cls="cv-welm">
        <p className="cv-lede" style={{ marginTop: 14 }}>Спасибо за доверие. Дальше всё видно здесь: график, фотоотчёты по этапам, акты и счета.</p>
        {people.length ? (
          <div className="cv-wel-team">
            {people.map((m) => (
              <div key={m.role} className="cv-wel-p">
                {m.photo ? <img className="cv-face" src={m.photo} alt="" /> : <span className="cv-face is-ini">{m.initials}</span>}
                <div><div className="n">{m.name}</div><div className="lab">{m.role}</div></div>
              </div>))}
          </div>) : null}
        {/* KSH-549: без чипов профессий (их было до 13) — в приветствии только менеджер и прораб; мастера видны в «Команде» на главной */}
        <div className="cv-wel-act">
          {w.projectReady
            ? <button type="button" className="btn-k" onClick={close}>Открыть кабинет ремонта</button>
            : <React.Fragment><span className="lab">Менеджер запускает ремонт — после этого здесь откроется кабинет ремонта.</span><button type="button" className="btn-k" onClick={close}>Понятно</button></React.Fragment>}
        </div>
      </Frame>);
  }
  /* показать ли приветствие сейчас: пора, ещё не видел, не сотрудник; перепроверяем при изменении документов и оплат сделки */
  function useWelcome(dealId) {
    useDealTick(['obitel:deal-docs']);
    const CC = window.ClientContract;
    const [closed, setClosed] = React.useState(false);
    const w = CC && dealId ? CC.welcome(dealId) : null;
    const show = !!(w && w.due && !w.seen && !isRO() && !closed);
    return [show, () => setClosed(true)];
  }
  /* кабинет ремонта (после запуска) монтирует приветствие отдельным компонентом — у App там свои ранние return */
  function WelcomeGate({ dealId, hidden }) {
    const [show, hide] = useWelcome(dealId);
    return show && !hidden ? <WelcomeWindow dealId={dealId} onClose={hide} /> : null;
  }

  function DocsWindow({ dealId, onClose }) {
    useDealTick(['obitel:deal-docs']);
    useCalcReady(dealId);
    const v = V();
    const d = v.docs(dealId);
    const subject = v.subject(dealId);
    const part = (title, rows) => rows.length > 0 && (
      <section style={{ marginTop: 28 }}>
        <h2 className="h2" style={{ fontSize: 20 }}>{title}</h2>
        <div className="cv-w cv-dlist2">{rows.map((r) => <DocCard key={r.id} project={subject} r={r} />)}</div>
      </section>);
    return (
      <Frame title="Документы" lab={d.waiting ? d.waiting + ' ' + plural(d.waiting, 'ждёт', 'ждут', 'ждут') + ' вашего действия' : 'от вас сейчас ничего не нужно'} onClose={onClose}>
        {isRO() ? <RoNote text="Режим «только чтение»: согласие за клиента отсюда не отправляется." /> : null}
        {part('Договоры и акты', d.contracts)}
        <ContractBrief dealId={dealId} />
        {part('Счета', d.bills)}
        {!d.contracts.length && !d.bills.length ? <div className="empty" style={{ marginTop: 28 }}>Документов пока нет — менеджер отправит договор, когда он будет готов.</div> : null}
      </Frame>);
  }

  const PAY_L = { planned: ['pill-d', 'после подписания договора'], due: ['pill-k', 'к оплате'], done: ['pill-g', 'оплачено'] };
  function PaymentsWindow({ dealId, onClose }) {
    useDealTick(['obitel:deal-docs']);
    useCalcReady(dealId);
    const m = V().money(dealId);
    return (
      <Frame title="Оплаты" lab="по договорам до запуска ремонта" onClose={onClose}>
        {isRO() ? <RoNote text="Режим «только чтение»: оплату отмечает менеджер." /> : null}
        {m.rows.length === 0
          ? <div className="empty" style={{ marginTop: 28 }}>Менеджер пришлёт договор — здесь появятся суммы и сроки оплаты.</div>
          : <React.Fragment>
            <div className="cv-kpis" style={{ marginTop: 24, paddingTop: 0, gap: 28 }}>
              <div><div className="lab">Оплачено</div><div className="stat num">{rub(m.paid)}</div><div className="bar"><i style={{ width: (m.total ? m.paid / m.total * 100 : 0) + '%' }} /></div><div className="lab" style={{ marginTop: 8 }}>отмечено менеджером</div></div>
              <div><div className="lab">К оплате сейчас</div><div className="stat num">{rub(m.due)}</div><div className="lab" style={{ marginTop: 18 }}>подписанные договоры</div></div>
              <div><div className="lab">Впереди</div><div className="stat num">{rub(m.total - m.paid - m.due)}</div><div className="lab" style={{ marginTop: 18 }}>после подписания</div></div>
            </div>
            <div style={{ marginTop: 28 }}>
              {m.rows.map((r) => (
                <div key={r.id} className="cv-payrow">
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 15 }}>{r.title}</div>
                    {r.state === 'done' && r.paidAt ? <div className="lab" style={{ marginTop: 3 }}>оплачено {r.paidAt}{r.verifiedAt ? ' · подтверждено ' + r.verifiedAt : ''}</div> : null}
                  </div>
                  <span className="num" style={{ fontSize: 18, whiteSpace: 'nowrap' }}>{r.amount ? rub(r.amount) : 'сумма уточняется'}</span>
                  <span className={PAY_L[r.state][0]}>{r.state === 'done' && <span className="dot" />}{PAY_L[r.state][1]}</span>
                </div>))}
            </div>
            <div className="note-s" style={{ marginTop: 20 }}><span>Оплатите по реквизитам из договора — менеджер отметит оплату.</span></div>
          </React.Fragment>}
      </Frame>);
  }

  function ChatWindow({ dealId, onClose }) {
    const [activeId, setActiveId] = React.useState(null);
    useDealTick(['obitel:deal-chat']);
    const v = V();
    const ro = isRO();
    const threads = v.chat(dealId);
    const active = threads.find((t) => t.id === activeId) || threads[0] || null;
    const subject = v.subject(dealId);
    const unread = window.DealChat ? window.DealChat.unread(dealId, 'client') : 0;
    /* открыл чат — прочитано (у сотрудников отметки нет); после отметки unread = 0, цикла нет */
    React.useEffect(() => { if (!ro && unread) threads.forEach((t) => window.DealChat.markRead(dealId, t.id, 'client')); }, [unread, ro]);
    /* тред «Клиент» создаётся при первом сообщении; отправка — через window.DealChat (у сотрудников это обёртка «только чтение») */
    const target = () => (active ? active.id : v.clientThread(dealId));
    const send = (tid, msg) => window.DealChat.send(dealId, tid, msg);
    /* до договора клиенту отвечает менеджер сделки (intake.team.manager) — его имя в честном сроке ответа (KSH-497) */
    const it = (window.loadSiteIntake && window.loadSiteIntake(dealId)) || {};
    const answerBy = () => (it.team && it.team.manager && it.team.manager.name) || '';
    return (
      <Frame title="Чат" lab="отвечаем пн–пт, 9:00–19:00" onClose={onClose} cls="cv-chatm">
        {ro ? <RoNote text="Режим «только чтение»: сообщения за клиента отсюда не отправляются." /> : null}
        <div className="cv-chat" style={threads.length > 1 ? undefined : { gridTemplateColumns: '1fr' }}>
          {threads.length > 1
            ? <div className="cv-threads">
              {threads.map((t) => (
                <button type="button" key={t.id} className={'cv-th' + (active && t.id === active.id ? ' on' : '')} onClick={() => setActiveId(t.id)}>
                  <span style={{ fontSize: 14 }}>{t.title}</span>
                  <span className="lab" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.last || 'нет сообщений'}</span>
                </button>))}
            </div>
            : null}
          <ChatConv project={subject} active={active} target={target} onSent={setActiveId} sendFn={send} readOnly={ro} noAttach answerBy={answerBy} empty="напишите менеджеру — он ответит здесь" />
        </div>
      </Frame>);
  }
  windows.docs = DocsWindow;
  windows.payments = PaymentsWindow;
  windows.chat = ChatWindow;

  /* ── окно «Смета» — коммерческое предложение (спека, «Смета как коммерческое предложение (КП)» и «Срок действия сметы») ──
     Модель — ClientOffer.view (lk/client-offer.js): все суммы приходят оттуда (а туда — из ClientEstimate.view, который сам прячет их у
     просроченного предложения); окно ничего не считает и не читает движок сметы. Решения клиента — ClientEstimate.decide/approve/rework,
     запрос новой сметы — EstimateOffer.request; у сотрудников («ЛК от имени») всё это отключено. Печать — @media print в client-v2.css */
  const EXPIRED_TEXT = 'Смета не актуальна — цены могли измениться. Свяжитесь с персональным менеджером';
  const RO_TITLE = 'только чтение';
  const DEC_L = { approved: 'согласовано', rejected: 'отказ' };
  const OFFER_GATE = 'Оплата открывает этап';

  function OfferWindow({ dealId, onClose, open }) {
    const [ver, tick] = React.useState(0);
    const [draft, setDraft] = React.useState(null); // отказ в работе: { id, text }, id — ключ решения (line.decisionKey / room.decisionKey)
    const [view, setView] = React.useState('all'); // вид сметы: 'all' — «Вся смета», 'rooms' — «По комнатам» (KSH-502)
    const [rw, setRw] = React.useState(null); // форма «На доработку»: null — закрыта, иначе текст
    const [flash, setFlash] = React.useState(''); // 'rework' — смета ушла на доработку
    const [err, setErr] = React.useState('');
    const [fail, setFail] = React.useState(false); // движок сметы не загрузился
    const [attempt, setAttempt] = React.useState(0);
    const E = window.ClientEstimate;
    const ready = !!(E && E.ready());
    const lazy = !!(window.LKLazy && window.LKLazy.calc);
    /* движок сметы на странице клиента грузится лениво: просим его сами и перерисовываемся, когда он готов; не загрузился — текст и «Повторить» */
    React.useEffect(() => {
      if (ready || !lazy) return undefined;
      let live = true;
      window.LKLazy.calc().then(() => { if (live) tick((x) => x + 1); }).catch(() => { if (live) setFail(true); });
      return () => { live = false; };
    }, [ready, attempt]);
    const ro = isRO();
    const O = window.ClientOffer;
    // вид КП считает и вид по комнатам: пересчёт — только после действия клиента или загрузки движка, не на каждый ввод в форме
    const k = React.useMemo(() => (ready && O ? O.view(dealId) : null), [ready, O, dealId, ver]);
    /* смета открыта клиентом: одно событие на открытие окна, номер открытия и часы с отправки — для сигналов менеджеру (KSH-497) */
    const opened = React.useRef(false);
    React.useEffect(() => {
      if (!k || opened.current) return;
      opened.current = true;
      const h = hoursSinceSent(dealId); const n = window.DealEvents ? window.DealEvents.count(dealId, 'offer_open') + 1 : 1;
      track(dealId, 'offer_open', Object.assign({ n, expired: !!k.cover.expired }, h == null ? {} : { hours: h }));
    }, [!!k]);
    const act = (fn) => {
      try { fn(); setErr(''); } catch (e) { setErr(e && e.message ? e.message : 'Не получилось — попробуйте ещё раз'); }
      tick((x) => x + 1);
    };
    const roProps = ro ? { disabled: true, title: RO_TITLE } : {};
    const ask = <button type="button" className="btn-l" onClick={() => open('chat')}>Задать вопрос менеджеру</button>;

    if (!ready && (fail || !lazy)) {
      return (
        <Frame title="Смета" lab="Коммерческое предложение" onClose={onClose} cls="cv-kpmod">
          <div className="empty" style={{ marginTop: 28 }}>Не удалось загрузить смету. Проверьте соединение и попробуйте ещё раз — или напишите менеджеру.</div>
          <div className="cv-kp-actions" style={{ marginTop: 20 }}>
            {lazy ? <button type="button" className="btn-k" onClick={() => { setFail(false); setAttempt((x) => x + 1); }}>Повторить</button> : null}
            {ask}
          </div>
        </Frame>);
    }
    if (!ready) return <Frame title="Смета" lab="Коммерческое предложение" onClose={onClose} cls="cv-kpmod"><div className="empty" style={{ marginTop: 28 }}>Смета загружается…</div></Frame>;
    if (!k) {
      return (
        <Frame title="Смета" lab="Коммерческое предложение" onClose={onClose} cls="cv-kpmod">
          <div className="empty" style={{ marginTop: 28 }}>{flash === 'rework'
            ? 'Смета отправлена на доработку менеджеру — он вернёт её вам, когда она будет готова.'
            : 'Смета сейчас недоступна — менеджер готовит новую версию.'}</div>
          <div className="cv-kp-actions" style={{ marginTop: 20 }}>{ask}</div>
        </Frame>);
    }

    const c = k.cover;
    const est = k.estimate;
    const exp = c.expired;
    const done = est.status === 'ready';
    const rejected = est.rejected;
    const members = [k.team.manager, k.team.foreman, k.team.designer].filter(Boolean);
    const gate = k.conditions.indexOf(OFFER_GATE) >= 0;
    // шкала времени: положение фазы между самой ранней и самой поздней датой графика
    const ms = (d) => Date.parse(d);
    const dated = k.phases.filter((p) => p.start && p.end);
    const t0 = dated.length ? Math.min.apply(null, dated.map((p) => ms(p.start))) : 0;
    const t1 = dated.length ? Math.max.apply(null, dated.map((p) => ms(p.end))) : 0;
    const span = Math.max(1, t1 - t0);
    const place = (p) => ({ left: ((ms(p.start) - t0) / span * 100) + '%', width: Math.max(2, (ms(p.end) - ms(p.start)) / span * 100) + '%' });

    // цены строк — без региональных коэффициентов и резерва, итог «Работы» — с ними: если фазы не сходятся с итогом, говорим об этом
    const phasesSum = est.phases.reduce((a, ph) => a + (ph.works || 0), 0);
    const lineRow = (l) => {
      const d = l.decision;
      const drafting = draft && draft.id === l.id;
      return (
        <div key={l.id} className="cv-kp-line">
          <div className="row">
            <div className="t"><span>{l.name}</span><span className="lab">{l.qty} {l.unit}</span></div>
            {l.sum != null ? <span className="num sum">{rub(l.sum)}</span> : null}
            <div className="dec">
              {d ? <span className={d.state === 'approved' ? 'pill-k' : 'pill-d'}>{DEC_L[d.state]}</span> : null}
              {est.canDecide && !d ? <button type="button" className="btn-l" {...roProps} onClick={() => act(() => E.decide(dealId, l.id, 'approved'))}>Согласовать</button> : null}
              {est.canDecide && !d ? <button type="button" className="btn-l" {...roProps} onClick={() => setDraft({ id: l.id, text: '' })}>Отказаться</button> : null}
              {est.canDecide && d ? <button type="button" className="btn-l" {...roProps} onClick={() => act(() => E.decide(dealId, l.id, null))}>Снять решение</button> : null}
            </div>
          </div>
          {l.roomRejects && l.roomRejects.length ? <div className="lab cv-er-rejn">отказ: {l.roomRejects.join(', ')}</div> : null}
          {d && d.comment ? <div className="lab cv-kp-cm">{d.comment}</div> : null}
          {drafting ? (
            <div className="cv-kp-rej cv-kp-noprint">
              <textarea className="cv-ta" rows={2} aria-label="Что не подходит" placeholder="Напишите, что не подходит" value={draft.text} onChange={(e) => setDraft({ id: l.id, text: e.target.value })} />
              <button type="button" className="btn-k" disabled={ro || !draft.text.trim()} title={ro ? RO_TITLE : undefined}
                onClick={() => act(() => { E.decide(dealId, l.id, 'rejected', draft.text); setDraft(null); track(dealId, 'offer_comment', { kind: 'line' }); })}>Сохранить отказ</button>
              <button type="button" className="btn-l" onClick={() => setDraft(null)}>Отмена</button>
            </div>) : null}
        </div>);
    };

    const approve = () => act(() => { E.approve(dealId); track(dealId, 'offer_approve', { n: window.DealEvents ? window.DealEvents.count(dealId, 'offer_open') : 0 }); });
    /* «Посмотреть, как это делаем мы» (KSH-495): материалы медиатеки «Для сделок» этой фазы (ClientOffer.view → phases[].media) и что скрыто в стенах */
    const howEl = (p) => {
      const media = p.media || [];
      if (!p.hidden && !media.length) return null;
      return (
        <details className="cv-kp-how cv-kp-noprint">
          <summary>Посмотреть, как это делаем мы</summary>
          <div className="cv-kp-howb">
          {media.length ? <div className="cv-kp-media">{media.map((m) => (
            <figure key={m.id} className={m.kind === 'video' && m.orient !== 'h' ? 'is-v' : ''}>
              {m.kind === 'video' ? <video src={m.url} controls playsInline preload="metadata" /> : <img src={m.url} alt={m.title || ''} loading="lazy" />}
              <figcaption className="lab">{[/\.[a-z0-9]{2,4}$/i.test(m.title || '') ? '' : m.title, m.obj].filter(Boolean).join(', ')}</figcaption>
            </figure>))}</div> : null}
          {p.hidden ? <div className="cv-kp-hid"><div className="lab">Что останется скрытым</div><ul>{p.hidden.items.map((t) => <li key={t}>{t}</li>)}</ul><p>{p.hidden.text}</p></div> : null}
          </div>
        </details>);
    };

    // «По комнатам» (lk/estimate-rooms-view.jsx): переключатель — только у сметы с комнатами; решения — по ключам line.decisionKey / room.decisionKey
    const RV = window.EstimateRoomsView, TG = window.EstimateViewToggle;
    const byRooms = est.byRooms && RV && TG ? est.byRooms : null;
    const showRooms = !!byRooms && view === 'rooms';
    const decide = (key, state) => act(() => E.decide(dealId, key, state));
    const rejectKey = (key, text) => act(() => { E.decide(dealId, key, 'rejected', text); setDraft(null); track(dealId, 'offer_comment', { kind: 'line' }); });
    const roomsEl = showRooms ? (
      <RV data={byRooms} rub={rub} hint={est.canDecide} btn={roProps}
        renderLine={(l) => <RV.LineDecision line={l} canDecide={est.canDecide} btn={roProps} draft={draft} setDraft={setDraft} onApprove={(key) => decide(key, 'approved')} onClear={(key) => decide(key, null)} onReject={rejectKey} />}
        onRoomReject={est.canDecide ? (r) => setDraft({ id: r.decisionKey, text: '' }) : undefined}
        onRoomClear={est.canDecide ? (r) => decide(r.decisionKey, null) : undefined}
        roomNote={(r) => <RV.RejectForm id={r.decisionKey} draft={draft} setDraft={setDraft} onSave={rejectKey} btn={roProps} />} />) : null;

    return (
      <Frame title="Смета" lab="Коммерческое предложение" onClose={onClose} cls="cv-kpmod">
        <div className="cv-kp">
          {ro ? <RoNote text="Режим «только чтение»: решения по смете за клиента отсюда не принимаются." /> : null}

          <section className="cv-kp-cover soft" aria-label="Обложка">
            <div className="lab">Коммерческое предложение</div>
            <h2 className="h1 cv-kp-name">{c.clientName || 'Ваш ремонт'}</h2>
            {c.address ? <div className="lab">{c.address}</div> : null}
            <div className="cv-kp-facts">
              <div><div className="lab">Итого</div><div className="stat num">{c.total == null ? '—' : rub(c.total)}</div>{c.total == null ? <div className="lab">сумма скрыта</div> : null}</div>
              {c.weeks ? <div><div className="lab">Срок ремонта</div><div className="stat num">≈ {c.weeks} нед.</div></div> : null}
            </div>
            {done ? <span className="pill-k"><span className="dot" />Смета согласована</span>
              : exp ? <span className="pill-d">Предложение истекло {c.untilR}</span>
              : c.untilR ? <span className="pill-g"><span className="dot" />Держим цены до {c.untilR}</span> : null}
            {exp ? (
              <div className="att cv-kp-banner" role="alert">
                <span>{EXPIRED_TEXT}</span>
                {est.offer.requested
                  ? <span className="pill-g"><span className="dot" />Запрос отправлен менеджеру</span>
                  : <button type="button" className="btn-k" {...roProps} onClick={() => act(() => { window.EstimateOffer.request(dealId, 'Клиент'); track(dealId, 'offer_refresh_request'); })}>Запросить актуальную смету</button>}
              </div>) : null}
          </section>

          {members.length > 0 ? (
            <section className="cv-kp-sec" aria-labelledby="cv-kp-team-h">
              <h2 className="h2" id="cv-kp-team-h">Ваша команда</h2>
              <div className="cv-kp-team">{members.map((m) => (
                <div key={m.chatRole} className="cv-kp-tm">
                  {m.photo ? <img className="cv-face" src={m.photo} alt="" width={56} height={56} style={{ width: 56, height: 56 }} />
                    : <span className="cv-face is-ini" style={{ width: 56, height: 56, fontSize: 18 }} aria-hidden="true">{m.initials}</span>}
                  <div><div className="n">{m.name}</div><div className="lab">{m.role}</div></div>
                </div>))}</div>
            </section>) : null}

          {k.cases.length > 0 ? (
            <section className="cv-kp-sec" aria-labelledby="cv-kp-cases-h">
              <h2 className="h2" id="cv-kp-cases-h">Похожие объекты</h2>
              <div className="cv-kp-cases">{k.cases.map((cs) => (
                <article key={cs.id || cs.title} className="cv-kp-case">
                  {cs.photo ? <img className="cv-kp-ph" src={cs.photo} alt={cs.title} /> : <div className="cv-kp-ph" aria-hidden="true"><span>Фото объекта</span></div>}
                  <div className="n">{cs.title}</div>
                  {cs.area || cs.weeks ? <div className="lab">{[cs.area ? cs.area + ' м²' : '', cs.weeks ? cs.weeks + ' нед.' : ''].filter(Boolean).join(' · ')}</div> : null}
                  {cs.text ? <p className="tx">{cs.text}</p> : null}
                </article>))}</div>
            </section>) : null}

          <section className="cv-kp-sec" aria-labelledby="cv-kp-tl-h">
            <h2 className="h2" id="cv-kp-tl-h">Как пойдёт ремонт</h2>
            <div className="lab" style={{ marginTop: 6 }}>Этапы работ и оплата по ним{gate ? ': оплата открывает этап' : ''}</div>
            <ol className="cv-kp-tl">
              <li><div className="hd"><b>Аванс {k.advance.pct}%</b>{k.advance.amount != null ? <span className="num">{rub(k.advance.amount)}</span> : null}</div><div className="lab">перед началом работ</div></li>
              {k.phases.map((p) => (
                <li key={p.name}>
                  <div className="hd"><b>{p.name}</b>{p.pay != null ? <span className="num">{rub(p.pay)}</span> : null}</div>
                  {p.start && p.end ? <div className="lab">{fmtDate(p.start)} — {fmtDate(p.end)}</div> : null}
                  {p.start && p.end ? <div className="bar cv-kp-track"><i style={place(p)} /></div> : null}
                  {howEl(p)}
                </li>))}
              {k.materials > 0 ? <li><div className="hd"><b>Материалы по спецификации</b><span className="num">{rub(k.materials)}</span></div></li> : null}
            </ol>
          </section>

          {k.promise && k.promise.length ? (
            <section className="cv-kp-sec" aria-labelledby="cv-kp-pm-h">
              <h2 className="h2" id="cv-kp-pm-h">Цена не меняется молча</h2>
              <ul className="cv-kp-pm">{k.promise.map((x) => <li key={x.title}><b>{x.title}</b><span>{x.text}</span></li>)}</ul>
            </section>) : null}

          {k.guarantees.length > 0 || k.conditions.length > 0 ? (
            <section className="cv-kp-sec" aria-labelledby="cv-kp-gr-h">
              <h2 className="h2" id="cv-kp-gr-h">Гарантии и условия</h2>
              <ul className="cv-kp-list">{k.guarantees.concat(k.conditions).map((t, i) => <li key={i}>{t}</li>)}</ul>
            </section>) : null}

          {k.about.text || k.about.advantages.length > 0 ? (
            <section className="cv-kp-sec" aria-labelledby="cv-kp-about-h">
              <h2 className="h2" id="cv-kp-about-h">О компании</h2>
              {k.about.text ? <p className="cv-kp-about">{k.about.text}</p> : null}
              {k.about.advantages.length > 0 ? <ul className="cv-kp-list">{k.about.advantages.map((t, i) => <li key={i}>{t}</li>)}</ul> : null}
            </section>) : null}

          <section className="cv-kp-sec cv-kp-est" aria-labelledby="cv-kp-est-h">
            <h2 className="h2" id="cv-kp-est-h">Подробная смета</h2>
            {byRooms ? <TG value={view} onChange={(v) => { setView(v); setDraft(null); }} /> : null}
            {est.total != null ? (
              <div className="cv-kp-tot">
                <div className="cv-tot"><span>Работы</span><span className="num">{rub(est.works)}</span></div>
                <div className="cv-tot"><span>Материалы</span><span className="num">{rub(est.materials)}</span></div>
                {est.discountPct > 0 ? <div className="cv-tot"><span>Скидка</span><span className="num">{est.discountPct}%</span></div> : null}
                <div className="cv-tot"><b>Итого</b><b className="num">{rub(est.total)}</b></div>
              </div>) : null}
            {!showRooms && est.total != null && phasesSum !== est.works ? <div className="lab" style={{ marginTop: 10 }}>Цены строк — без региональных коэффициентов и резерва; итог «Работы» — с ними</div> : null}
            {est.canDecide ? <div className="lab" style={{ marginTop: 14 }}>Согласовано строк: {est.approved} · отказов: {rejected}</div> : null}
            {showRooms ? roomsEl : est.phases.map((ph) => {
              // строки не обязаны давать итог фазы: в него входят и шаги пакетов, и ручные позиции — их среди строк нет
              const lineSum = ph.stages.reduce((a, st) => a + st.lines.reduce((b, l) => b + (l.sum || 0), 0), 0);
              return (
                <div key={ph.name} className="cv-kp-phase">
                  <div className="hd"><h3 className="cv-kp-h3">{ph.name}</h3>{ph.works != null ? <span className="num">{rub(ph.works)}</span> : null}</div>
                  {ph.works != null && lineSum !== ph.works ? <div className="lab">Итог фазы включает не только строки ниже, в том числе пакеты и позиции вне списка</div> : null}
                  {ph.stages.map((st) => (
                    <div key={st.name} className="cv-kp-stage">
                      <div className="lab st">{st.name}</div>
                      {st.lines.map(lineRow)}
                    </div>))}
                </div>);
            })}

            {err ? <div className="note-s" role="alert" style={{ marginTop: 16 }}>{err}</div> : null}
            <div className="cv-kp-actions cv-kp-noprint">
              {est.canDecide ? <button type="button" className="btn-k" disabled={ro || rejected > 0} title={ro ? RO_TITLE : undefined} onClick={approve}>Согласовать смету</button> : null}
              {est.canDecide ? <button type="button" className="btn-l" {...roProps} onClick={() => setRw('')}>На доработку</button> : null}
              {ask}
            </div>
            {est.canDecide && rejected > 0 ? <div className="lab cv-kp-noprint" style={{ marginTop: 10 }}>Снимите {rejected} отказ(ов), чтобы согласовать смету</div> : null}
            {rw !== null ? (
              <div className="cv-kp-rej cv-kp-noprint">
                <textarea className="cv-ta" rows={3} aria-label="Что нужно изменить" placeholder="Что нужно изменить (по желанию)" value={rw} onChange={(e) => setRw(e.target.value)} />
                <button type="button" className="btn-k" {...roProps} onClick={() => act(() => { E.rework(dealId, rw); setRw(null); setFlash('rework'); track(dealId, 'offer_comment', { kind: 'rework' }); })}>Отправить на доработку</button>
                <button type="button" className="btn-l" onClick={() => setRw(null)}>Отмена</button>
              </div>) : null}
          </section>

          <section className="cv-kp-sec cv-kp-noprint" aria-labelledby="cv-kp-pr-h">
            <h2 className="h2" id="cv-kp-pr-h">Версия для печати</h2>
            <p className="cv-mtext">Распечатайте предложение или сохраните его в PDF средствами браузера.</p>
            <button type="button" className="btn-l" onClick={() => window.print()}>Версия для печати</button>
          </section>

          {est.canDecide && !exp && est.total != null ? (
            <div className="cv-kp-sticky cv-kp-noprint" role="region" aria-label="Итог и согласование">
              <div><div className="lab">Итого{c.untilR ? ', цены до ' + c.untilR : ''}</div><b className="num">{rub(est.total)}</b></div>
              <button type="button" className="btn-k" disabled={ro || rejected > 0} title={ro ? RO_TITLE : rejected > 0 ? 'Сначала снимите отказы в строках' : undefined} onClick={approve}>Согласовать смету</button>
            </div>) : null}
        </div>
      </Frame>);
  }
  windows.estimate = OfferWindow;

  /* ссылка анкеты: client-v2.html?deal=D&form=<токен> (такие ссылки уже разосланы клиентам со старых адресов анкет) */
  const formParam = (search) => new URLSearchParams(search || '').get('form') || '';
  /* ссылка анкеты открывает своё окно один раз за вкладку: закрыли — «Главная» и другие переходы (ссылка сохраняет ?form=, он же
     даёт клиенту доступ) окно заново не открывают. Отметка — в sessionStorage вкладки на пару «сделка + токен»; явное открытие
     (пункт меню, главное действие, хэш) работает как обычно */
  const seenKey = (dealId, tok) => 'cv_form_seen:' + dealId + ':' + tok;
  const formSeen = (dealId, tok) => { try { return !!window.sessionStorage.getItem(seenKey(dealId, tok)); } catch (e) { return false; } };
  const markFormSeen = (dealId, tok) => { try { window.sessionStorage.setItem(seenKey(dealId, tok), '1'); } catch (e) { /* без отметки окно откроется снова — не страшно */ } };
  function PreWindow({ dealId, win, open, onClose }) {
    const W = windows[win];
    if (W) return <W dealId={dealId} open={open} onClose={onClose} />;
    return <Frame title={TITLES[win] || 'Раздел'} onClose={onClose}><div className="empty" style={{ marginTop: 28 }}>Этот раздел скоро откроется здесь.</div></Frame>;
  }

  /* маршрут до запуска: адрес → окно по сделке (ClientDealViews.windowFor). '' — главная (лишний адрес очищаем), 'design' — страница
     дизайн-проекта с той же ссылкой, остальное — окно поверх главной. Без хэша ссылка анкеты ?form=<токен> открывает своё окно
     (ClientDealViews.formWindow); закрытие окна запоминается во вкладке (formSeen), чтобы оно не открылось снова */
  function PreApp({ dealId, modal, open, close: closeHash }) {
    const [formTok, setFormTok] = React.useState(() => { const t = formParam(window.location.search); return t && !formSeen(dealId, t) ? t : ''; });
    const win = modal ? V().windowFor(dealId, modal) : (formTok ? V().formWindow(dealId, formTok) : '');
    /* визит в кабинет до старта: одно событие на вкладку (sessionStorage), чтобы считать возвраты между шагами, а не перерисовки */
    React.useEffect(() => {
      const k = 'cv_pre_open:' + dealId;
      try { if (window.sessionStorage.getItem(k)) return; window.sessionStorage.setItem(k, '1'); } catch (e) { /* без отметки — событие на каждую загрузку */ }
      track(dealId, 'lk_pre_open', { via: formTok ? 'form' : (modal ? 'link' : 'home') });
    }, [dealId]);
    const close = () => {
      closeHash();
      if (formTok) { markFormSeen(dealId, formTok); setFormTok(''); }
    };
    React.useEffect(() => { if (modal && !win) close(); }, [modal, win]);
    if (win === 'design') { window.location.replace(designHref(modal.indexOf('design-') === 0 ? modal.slice(7) || 'stages' : 'stages')); return null; }
    return (
      <React.Fragment>
        <PreHome dealId={dealId} open={open} />
        {win ? <PreWindow dealId={dealId} win={win} open={open} onClose={close} /> : null}
        <WelcomeGate dealId={dealId} hidden={!!win} />
      </React.Fragment>);
  }

  CV.Pre = { PreHome, PreSteps, PreApp, PreWindow, Frame, windows, TITLES, MeasureWindow, BriefWindow, DocsWindow, PaymentsWindow, ChatWindow, formParam, ContractBrief, WelcomeWindow, WelcomeGate };
  if (window.__CV_PAGE === 'prestart') ReactDOM.createRoot(document.getElementById('root')).render(<CV.App />);
})();
