/* foreman-v2-app.jsx — кабинет прораба «центр управления объектами» в виде кабинета клиента (client-v2).
   Собран из библиотеки виджетов lk-widgets.jsx (window.LKW) на данных foreman-views.js (window.ForemanViews).
   Главная: сводка по всем объектам, «Сейчас важно», плитки объектов, календарь всех объектов, приёмка, чаты, график (Гант), команда.
   Окна по #hash: #objects, #obj=<id> (объект в формате кабинета клиента), #gantt, #team, #team=<id>, #accept, #calendar, #chat, #chat=<pid>:<tid>,
   #work=<pid>~<taskId>[~<pointId>][~<фото>] — карточка работы (lk-workcard.jsx): фото с замечаниями, решение, история, наряд (кто делает).
   #supply[=<pid>] — материалы и доставки (lk-supply.jsx), #delivery=<pid>~<заказ> — приёмка поставки на объекте.
   #spec=<pid> — спецификация объекта по этапам графика (lk-spec.jsx): сроки решить / заказать / привезти, этапы «под угрозой».
   #docs[=<pid>] — документы и акты (lk-docs.jsx): закрыть этап, акты у клиента, акты и оплаты по фазам, пакет сдачи.
   #photos[=<pid>] — фото для клиента (lk-photos.jsx): принятые работы без фото, сданные с фото, отчёт понедельника, что видит клиент.
   #hidden[=<pid>] — скрытые работы (lk-hidden.jsx): пункты «скрытые коммуникации» без снимков, своё фото прораба, что получит клиент.
   #subs[=<pid>] — подрядчики (lk-subs.jsx): заказы подрядчикам — счёт, заказ, поставка, ссылка подрядчику, отмена; оплаты подрядчикам.
   По комнатам (KSH-504): в карточке работы — шаги по комнатам (acceptRoomStep / returnRoomStep, «Отметить сделанным» — submit + accept),
   в окне объекта — готовность по комнатам (roomReadiness), в приёмке — «сдано, ждёт проверки» по комнатам (#accept=rooms — сразу этот фильтр).
   Действия — функции ProjectStore, что и в старом кабинете: принять или вернуть работу (setTaskStatus / decideGate, возврат — с замечаниями
   к фото), принять объект (accept), написать в чат объекта (chatSend), принять поставку (acceptDelivery) и оформить возврат (createReturn),
   выдать наряд или сменить мастера (assign) и отправить мастеру ссылку на задание (AccessStore.ensureToken → executor-task.html), закрыть этап (closeStage — акт фазы и счёт клиенту), собрать пакет сдачи (addHandoverDoc / removeHandoverDoc), добавить фото к принятой работе (addAttachment), снять скрытые работы (addHiddenComm), вести заказы подрядчикам (enterSubInvoice, orderSub,
   deliverSub, inviteSub, cancelSubOrder). Демо (foreman-v2-demo.html) — объекты прораба из foreman-demo.js, только в памяти вкладки. */
(function () {
  'use strict';
  const P = window.ProjectStore, FV = window.ForemanViews, W = window.LKW, A = window.AccessStore, St = window.StaffStore;
  const { Ic, Face, FaceSt, teamSt, Header, Modal, FactsBand, NowCards, CalendarW, CalAgenda, CalTabs, calObjs, countBy, RowsW, ChatsW, ObjTile, GanttW, TeamW, rub, plural, cap, dateR, short, isoAdd, leftL } = W;
  const Lightbox = window.LKLightbox;
  const DEMO = /-demo(\.html)?$/.test(window.location.pathname);
  const t0 = () => new Date().toISOString().slice(0, 10);

  // кто смотрит: аккаунт прораба; в демо и у руководителя — прораб демо или все объекты
  function useMe() {
    const acc = A && A.currentAccount ? A.currentAccount() : null;
    const id = acc && acc.role === 'foreman' ? acc.profileId : DEMO ? 'stf-demo-f' : null;
    const s = id && St && St.get ? St.get(id) : null;
    return { id, name: (s && s.name) || (acc && acc.name) || 'Прораб', photo: (s && s.photoUrl) || '', role: acc && acc.role === 'super' ? 'super' : 'foreman' };
  }
  function useStore() {
    const [, tick] = React.useState(0);
    React.useEffect(() => (P ? P.subscribe(() => tick((x) => x + 1)) : undefined), []);
    // отпуска и статусы штата (StaffStore) — тоже перерисовка
    React.useEffect(() => { const on = () => tick((x) => x + 1); window.addEventListener('obitel:staff', on); return () => window.removeEventListener('obitel:staff', on); }, []);
  }
  function useHash() {
    const read = () => decodeURIComponent((window.location.hash || '').replace('#', ''));
    const [m, setM] = React.useState(read());
    React.useEffect(() => { const on = () => setM(read()); window.addEventListener('hashchange', on); return () => window.removeEventListener('hashchange', on); }, []);
    const open = (id) => { window.location.hash = id; };
    const close = () => { history.replaceState(null, '', window.location.pathname + window.location.search); setM(''); };
    return [m, open, close];
  }
  const greeting = () => { const h = new Date().getHours(); return h < 5 ? 'Доброй ночи' : h < 12 ? 'Доброе утро' : h < 18 ? 'Добрый день' : 'Добрый вечер'; };
  const todayLine = () => cap(new Date().toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' }));
  const ago = (iso) => { if (!iso) return ''; const h = Math.round((Date.now() - Date.parse(iso)) / 3600000); return h < 1 ? 'только что' : h < 24 ? h + ' ч назад' : h < 48 ? 'вчера' : Math.round(h / 24) + ' ' + plural(Math.round(h / 24), 'день', 'дня', 'дней') + ' назад'; };
  const when = (iso) => { if (!iso) return ''; const d = iso.slice(0, 10); return d === t0() ? new Date(iso).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) : dateR(d, { month: 'short' }); };

  const NAV = [['', 'Главная'], ['objects', 'Объекты'], ['week', 'Календарь'], ['gantt', 'График'], ['team', 'Команда'], ['accept', 'Приёмка'], ['supply', 'Материалы']];
  // разделы, которые ещё живут на страницах прежнего кабинета (в демо их нет — там другая база)
  // нижняя панель на телефоне (KSH-550): главное для прораба на объекте; Календарь, График, Команда — в «Ещё»
  const PHONE = ['', 'objects', 'accept', 'supply'];
  const MORE = [['docs', 'Документы и акты'], ['photos', 'Фото для клиента'], ['hidden', 'Скрытые работы'], ['subs', 'Подрядчики'], ['prices', 'Цены мастеров'], ['calendar', 'События объектов'], ['chat', 'Чаты'], ['tools.html?role=foreman', 'Инструменты']].concat(DEMO ? [] : [['foreman-estimates.html', 'Сметы'], ['foreman-notes.html', 'Заметки'], ['foreman-profile.html', 'Профиль']]);

  // окно карточки работы: объект, работа, точка фотоконтроля (необязательно), какое фото открыть
  const workHash = (x, at) => 'work=' + [x.pid, x.taskId, x.pointId || '', at != null ? at : ''].join('~').replace(/~+$/, '');

  /* ───── приёмка: принять сразу; вернуть — в карточке работы, с замечаниями к фото (те же переходы статуса, что в старом кабинете) ───── */
  function useDecide(me) {
    const [err, setErr] = React.useState('');
    const actor = { role: 'foreman', name: me.name };
    const accept = (r) => { try { if (r.pointId) P.decideGate(r.pid, r.taskId, r.pointId, 'approve', '', actor); else P.setTaskStatus(r.pid, r.taskId, 'done', { actor }); setErr(''); } catch (e) { setErr(e.message); } };
    return { accept, err, actor };
  }
  function DecideActs({ r, d, open }) {
    return (
      <React.Fragment>
        <button type="button" className="lw-btn-l" onClick={() => open(workHash(r))}>Вернуть</button>
        <button type="button" className="lw-btn-k" onClick={() => d.accept(r)}><Ic n="check" s={14} />Принять</button>
      </React.Fragment>);
  }
  const acceptRows = (rows, d, open) => rows.map((r) => ({
    key: r.pid + r.taskId + (r.pointId || ''), title: r.title + (r.kind === 'gate' && r.stage ? ' · ' + r.stage : ''), sub: [short(r.obj), r.kind === 'gate' ? 'фотоконтроль' : r.stage, r.who ? 'сдал ' + r.who : '', r.at ? ago(r.at) : ''].filter(Boolean).join(' · '),
    thumb: r.photos[0] ? r.photos[0].url : null, thumbN: r.photos.length, onThumb: () => open(workHash(r, 0)),
    onClick: () => open(workHash(r)),
    acts: <DecideActs r={r} d={d} open={open} />,
  }));
  // grow — на главной блок тянется по высоте колонки (календарь и приёмка выше на 35 %, Роман 29.10), список прокручивается внутри
  function AcceptW({ rows, d, open, onOpen, grow }) {
    return <RowsW id="lw-acc-h" title="Приёмка и согласования" n={rows.length} rows={acceptRows(rows, d, open)} empty="Все сданные работы приняты." onOpen={onOpen} text={onOpen ? 'Вся приёмка' : null} className={'lw-acc' + (grow ? ' is-grow' : '')} max={grow ? null : 330} />;
  }

  /* «Сейчас важно» по всем объектам: возвраты, просрочки, новый объект, визиты, доставки, оплаты, которые держат работы */
  const NOWK = { returned: 'Возврат на доработку', nobody: 'Нет мастера', hidden: 'Скрытые работы', sub: 'Подрядчик', price: 'Цена мастера', ask: 'Вопрос по наряду', agree: 'Ждёт согласия мастера', shift: 'Перенос сроков', late: 'Просрочено', handoff: 'Новый объект', visit: 'Визит клиента', delivery: 'Принять поставку', close: 'Закрыть этап', hold: 'Ждём оплату клиента', act: 'Акт у клиента' };
  // inObj — окно объекта: имя объекта в карточке не повторяем
  function nowRows(q, open, limit, inObj) {
    return q.filter((x) => x.kind !== 'accept' && x.kind !== 'gate').slice(0, limit || 3).map((x) => ({
      key: x.kind + x.pid + (x.taskId || x.refId || ''), kindL: x.kind === 'delivery' && !x.poId ? 'Доставка' : NOWK[x.kind] || x.kind, late: x.kind === 'late',
      right: x.kind === 'late' ? 'на ' + x.overdueDays + ' ' + plural(x.overdueDays, 'день', 'дня', 'дней') : x.kind === 'delivery' && x.dueIso <= isoAdd(t0(), 1) ? (x.dueIso <= t0() ? 'сегодня' : 'завтра') : x.dueIso ? dateR(x.dueIso) : x.kind === 'handoff' || inObj ? '' : short(x.obj),
      title: x.kind === 'hold' ? rub(x.amount) : x.kind === 'handoff' ? x.obj : x.title,
      sub: x.kind === 'hold' ? x.title + ' · ' + x.sub.replace('Ждём оплату клиента · ', '') : x.kind === 'handoff' ? x.sub : x.kind === 'returned' ? x.sub.replace('Вернули на доработку: ', '') : inObj ? x.sub : short(x.obj) + (x.sub ? ' · ' + x.sub : ''),
      // возврат, просрочка и работа без мастера — конкретная работа: «Открыть» («Выдать наряд», мастер ушёл — «Передать другому») ведёт в её карточку; поставка в пути — в приёмку,
      // доставка — в материалы объекта; этап к закрытию и акт у клиента — в документы объекта
      card: !!(x.taskId && (x.kind === 'returned' || x.kind === 'late' || x.kind === 'nobody' || x.kind === 'ask' || x.kind === 'agree' || x.kind === 'shift')) || x.kind === 'delivery' || x.kind === 'close' || x.kind === 'hidden' || x.kind === 'sub' || x.kind === 'price',
      act: x.kind === 'delivery' ? { text: x.poId ? 'Принять' : 'Открыть', onClick: () => open(x.poId ? 'delivery=' + x.pid + '~' + x.poId : 'supply=' + x.pid) }
        : x.kind === 'close' || x.kind === 'act' ? { text: x.kind === 'close' ? 'Закрыть' : 'Открыть', onClick: () => open('docs=' + x.pid) }
        : x.kind === 'hidden' ? { text: 'Снять', onClick: () => open('hidden=' + x.pid) }
        : x.kind === 'sub' ? { text: x.act || 'Открыть', onClick: () => open('subs=' + x.pid) }
        : x.kind === 'price' ? { text: 'Решить', onClick: () => open('prices=' + x.pid) }
        // вопрос мастера по наряду — в чат задачи (тот же тред, куда мастер написал вопрос); ждём согласия — в карточку работы
        : x.kind === 'ask' ? { text: 'Ответить', onClick: () => window.LKDiscussTask({ pid: x.pid, taskId: x.taskId, actor: { role: 'foreman', name: 'Прораб' } }) }
        : x.kind === 'agree' ? { text: 'Открыть', onClick: () => open(workHash(x)) }
        // мастер просит перенести сроки (KSH-528) — решение в карточке работы: там видно, чьи работы заденет
        : x.kind === 'shift' ? { text: 'Решить', onClick: () => open(workHash(x)) }
        : { text: x.kind === 'handoff' ? 'Посмотреть и принять' : x.kind === 'nobody' ? (x.gone ? 'Передать другому' : 'Выдать наряд') : 'Открыть', onClick: () => open(x.taskId && (x.kind === 'returned' || x.kind === 'late' || x.kind === 'nobody') ? workHash(x) : 'obj=' + x.pid) },
    }));
  }
  /* «Мои задачи» прораба (Роман 08.11.2026: «не плодить формы — собрать все задачи в одном месте, в таск-менеджере; доработку соединить
     с таск-менеджером»). «Мне сделать» — та же очередь, что и «Сейчас важно» (FV.queue: приёмка, фотоконтроль, перенос сроков, вопросы
     по наряду, цены мастеров, поставки, этапы к закрытию…) плюс задачи сделок (TaskViews.inbox: возвраты сметы и документов).
     Возвраты, которые прораб сам отправил, здесь не повторяются — они во вкладке «Отправлено мной». Новых процессов нет: строка ведёт
     туда же, куда кнопка карточки «Сейчас важно» (act) */
  const QK = Object.assign({}, NOWK, { accept: 'Приёмка', gate: 'Фотоконтроль' });
  function myTaskRows(q, open) {
    const acts = {};
    nowRows(q.filter((x) => x.kind !== 'accept' && x.kind !== 'gate'), open, 1e9).forEach((r) => { acts[r.key] = r.act; });
    return q.filter((x) => x.kind !== 'returned').map((x) => {
      const key = x.kind + x.pid + (x.taskId || x.refId || '');
      const act = x.kind === 'accept' || x.kind === 'gate' ? { text: 'Принять', onClick: () => open(workHash(x)) } : acts[key] || { text: 'Открыть', onClick: () => open('obj=' + x.pid) };
      const wait = x.kind === 'agree'; // наряд выдан, ждём согласия мастера — следующий шаг не за прорабом
      return {
        id: 'fq:' + key + (x.pointId ? ':' + x.pointId : ''), kind: x.kind, kindL: QK[x.kind] || x.kind,
        title: x.kind === 'hold' ? x.title + ' · ' + rub(x.amount) : x.kind === 'handoff' ? x.obj : x.title,
        obj: x.kind === 'handoff' ? '' : x.obj, pid: x.pid, taskId: x.taskId || undefined, pointId: x.pointId || undefined,
        sub: x.kind === 'accept' ? 'Сдал ' + (x.who || 'мастер') : x.kind === 'gate' ? 'Фотоконтроль · ' + (x.stage || '') : x.kind === 'late' ? '' : x.sub || '',
        due: x.kind === 'late' ? undefined : x.dueIso || undefined, now: x.kind === 'accept' || x.kind === 'gate' || undefined,
        late: x.kind === 'late' || undefined, lateDays: x.kind === 'late' ? x.overdueDays : undefined,
        status: 'open', statusL: QK[x.kind] || '', next: { who: wait ? 'other' : 'me', l: wait ? 'Ждём мастера' : act.text }, act: act.onClick,
      };
    });
  }
  // «Мне сделать»: очередь прораба + задачи сделок (TaskViews.inbox добавляет путь «Фаза · Этап»)
  function myInbox(q, open, me) {
    const T = window.TaskViews;
    const deal = T && T.inbox ? T.inbox('foreman', me.name || 'foreman', t0()) : [];
    const rows = myTaskRows(q, open);
    return (T && T.withPath ? T.withPath(rows) : rows).concat(deal);
  }
  // событие календаря: работа — её карточка, доставка — материалы объекта, остальное — объект
  const pickEvent = (open) => (e) => open(e.kind === 'work' && e.refId ? workHash({ pid: e.pid, taskId: e.refId }) : e.kind === 'delivery' ? 'supply=' + e.pid : 'obj=' + e.pid);

  /* ───── главная ───── */
  function Home({ me, data, open, view, d }) {
    const { objs, inc, q, tm, cal, gt, chats, rev, sum } = data;
    const live = objs.flatMap((o) => o.onSite.map((n) => ({ n, obj: o.title })));
    const lastDone = P.list().flatMap((p) => p.tasks.filter((t) => t.status === 'done' && t.doneAt).map((t) => ({ t, p }))).sort((a, b) => String(b.t.doneAt).localeCompare(String(a.t.doneAt)))[0];
    const first = (me.name || '').split(' ')[0];
    const g0 = isoAdd(t0(), -21), g1 = isoAdd(t0(), 7 * 22);
    return (
      <div className="cv-page lw-page">
        <Header nav={NAV} more={MORE} phone={PHONE} cur="" right={<React.Fragment>{window.__FOREMAN_DEMO_AUTOBUILD && <span className="pill-g" title="Демо: объекты живут только в этой вкладке">Демо · не сохраняется</span>}<span className="cv-ava" title={me.name}>{me.name.split(' ').map((w) => w[0]).join('').slice(0, 2)}</span></React.Fragment>} />
        <main id="main" tabIndex={-1} className="cv-main">
          <section className="cv-hero">
            <div><div className="lab">{todayLine()} · {sum.objects} {plural(sum.objects, 'объект', 'объекта', 'объектов')} в работе</div><h1 className="cv-hello">{greeting()}{first ? ', ' + first : ''}</h1></div>
            <div className="cv-hero-act">
              {sum.review
                ? <React.Fragment><button type="button" onClick={() => open('accept')} className="btn-k">Ждут вашей приёмки · {sum.review}</button><button type="button" onClick={() => open('accept')} aria-label="Открыть приёмку" className="ic ic-k"><Ic n="arrow" c="#fff" /></button></React.Fragment>
                : <span className="pill-g"><span className="dot" />Все сданные работы приняты</span>}
            </div>
          </section>

          <FactsBand label="Сводка по объектам"
            live={<React.Fragment>
              <span className="now">{live.length ? <React.Fragment><i className="cv-pulse" aria-hidden="true" /><span>Сейчас на объектах: {live.map((x, i) => <React.Fragment key={i}>{i ? ', ' : ''}{x.n} <span className="lw-mut">({short(x.obj)})</span></React.Fragment>)}</span></React.Fragment> : 'Сейчас работ на объектах нет'}</span>
              {lastDone && <span className="last">Последняя приёмка — {ago(lastDone.t.doneAt)}, «{lastDone.t.name}», {short(lastDone.p.title)}</span>}
            </React.Fragment>}
            facts={[
              { v: sum.objects + ' ' + plural(sum.objects, 'объект', 'объекта', 'объектов'), lab: inc.length ? 'В работе · ещё ' + inc.length + ' ждёт приёма' : 'В работе', onClick: () => open('objects') },
              { v: sum.review + ' на приёмке', lab: [rev.filter((r) => r.kind === 'gate').length ? 'фотоконтроль' : '', data.roomN ? 'по комнатам ждут проверки: ' + data.roomN : '', q.filter((x) => x.kind === 'returned').length + ' ' + plural(q.filter((x) => x.kind === 'returned').length, 'возврат', 'возврата', 'возвратов'), sum.late ? sum.late + ' просрочено' : 'без просрочек'].filter(Boolean).join(' · '), onClick: () => open('accept') },
              { v: sum.nearest ? dateR(sum.nearest.dueIso) : '—', lab: sum.nearest ? 'Ближайшая сдача · ' + short(sum.nearest.title) + ', ' + leftL(sum.nearest.daysLeft) : 'Сдач не запланировано', onClick: () => open('gantt') },
              { v: rub(sum.outWeek), lab: 'Выработка за 7 дней · с начала месяца ' + rub(sum.outMonth), onClick: () => open('team') },
              { v: sum.avgLoad + ' %', lab: 'Загрузка бригады · ' + sum.people + ' ' + plural(sum.people, 'человек', 'человека', 'человек'), onClick: () => open('team') },
            ]} />

          <NowCards rows={nowRows(q, open)} />

          <section className="lw-sec" aria-labelledby="lw-obj-h">
            {/* на главной — один ряд из четырёх плиток (новый объект на приём — всегда в нём), остальные в «Все объекты» */}
            <div className="lw-sech"><h2 className="h2" id="lw-obj-h">Мои объекты</h2><button type="button" className="cv-arrow-btn" onClick={() => open('objects')}>Все объекты{objs.length + inc.length > 4 ? ' · ' + (objs.length + inc.length) : ''}<Ic n="ne" s={14} /></button></div>
            <div className="lw-tiles">
              {objs.slice(0, 4 - Math.min(inc.length, 1)).map((o) => <ObjTile key={o.id} o={o} onOpen={() => open('obj=' + o.id)} />)}
              {inc.slice(0, 1).map((p) => <IncomingTile key={p.id} p={p} open={open} />)}
            </div>
          </section>

          <section className="cv-two lw-two">
            <CalendarW c={cal} title="Календарь объектов" onOpen={() => open('calendar')} onPick={pickEvent(open)} />
            <div className="cv-side"><AcceptW rows={rev} d={d} open={open} onOpen={() => open('accept')} grow /><ChatsW chats={chats.map((c) => ({ ...c, when: when(c.at) }))} onOpen={() => open('chat')} onPick={(c) => open('chat=' + c.pid + ':' + c.id)} /></div>
          </section>

          {/* «Мои задачи» (KSH-539): все задачи прораба в одном таск-менеджере — «Мне сделать» и «Отправлено мной» (возвраты на доработку, KSH-530).
              Отдельного блока «Отправлено на доработку» больше нет. Принять или вернуть — в окне работы, как и раньше */}
          {window.TaskViews && window.TaskViews.outbox && <section className="cv-w lw-gw lw-scope" aria-label="Мои задачи">
            <W.TaskList title="Мои задачи" t0={t0()} onOpen={(r) => (r.act ? r.act() : r.taskId ? open(workHash(r)) : r.href ? (window.location.href = r.href) : open('obj=' + r.pid))}
              tabs={[
                { id: 'in', label: 'Мне сделать', rows: myInbox(q, open, me), empty: 'Сейчас на вас ничего нет.', columns: window.TaskViews.IN_COLUMNS, colOf: window.TaskViews.inCol },
                { id: 'out', label: 'Отправлено мной', rows: window.TaskViews.outbox({ role: 'foreman', name: me.name }, t0()), empty: 'Вы ничего не возвращали на доработку — или всё уже принято.',
                  buckets: window.TaskViews.OUT_BUCKETS, bucketOf: window.TaskViews.outBucket,
                  // вид «Канбан» (KSH-533): те же строки и принятые за 3 дня — по колонкам статусов
                  boardRows: window.TaskViews.outbox({ role: 'foreman', name: me.name }, t0(), { closed: true }), columns: window.TaskViews.OUT_COLUMNS, colOf: window.TaskViews.outCol }]} />
          </section>}

          {/* календарь работ (KSH-525): эта неделя по объектам, сверху — что завтра */}
          <section className="cv-w lw-gw lw-wkw" aria-labelledby="lw-wk-h">
            <W.WHead id="lw-wk-h" title="Календарь работ" sub={'Завтра, ' + W.nextLine(data.wk.next).replace(/^./, (c) => c.toLowerCase())} text="Весь календарь" onOpen={() => open('week')} />
            <W.WeekW w={data.wk7} by="obj" max={2} onRow={(r) => open('obj=' + r.pid)} onWork={(it) => open(workHash({ pid: it.pid, taskId: it.taskId }))} />
          </section>

          <section className="cv-w lw-gw" aria-labelledby="lw-gt-h">
            <W.WHead id="lw-gt-h" title="График работ" sub={'фазы по объектам · ' + dateR(g0, { month: 'short' }) + ' — ' + dateR(g1, { month: 'short', year: 'numeric' })} text="Весь график" onOpen={() => open('gantt')} />
            <GanttW g={gt} from={g0} to={g1} onRow={(r) => open('obj=' + r.id)} />
          </section>

          <TeamW t={tm} onOpen={() => open('team')} onPerson={(p) => open('team=' + p.id)} />
        </main>
      </div>);
  }
  function IncomingTile({ p, open }) {
    return (
      <button type="button" className="lw-tile is-new" onClick={() => open('obj=' + p.id)} aria-label={'Новый объект ' + p.title + ': посмотреть и принять'}>
        <span className="lw-tph"><span className="ph0"><Ic n="key" s={30} /><span className="lab">Новый объект ждёт приёма</span></span></span>
        <span className="tx"><span className="t">{p.title}</span><span className="lab">{[p.client && p.client.name, p.area ? p.area + ' м²' : ''].filter(Boolean).join(' · ')}</span>
          <span className="row"><span className="st">{p.tasks.filter((t) => t.kind !== 'milestone').length} работ · {rub(p.totals.works || 0)}</span></span>
          <span className="cv-arrow-btn lw-tgo">Посмотреть и принять<Ic n="ne" s={14} /></span></span>
      </button>);
  }

  // график объекта: этапы по фазам, события объекта, шкала дни / недели / месяцы (по умолчанию — дни)
  // свои события на объекте (Роман, 07.11): прораб ставит их сам — ProjectStore.addSiteEvent / removeSiteEvent
  const FME = { role: 'foreman', name: 'Прораб' };
  const addEv = (r, o) => P.addSiteEvent(r.id, { title: o.title, date: o.date, end: o.end, note: o.note, client: o.client }, FME);
  const rmEv = (r, evId) => P.removeSiteEvent(r.id, evId, FME);
  function ObjGantt({ gt, onWork }) {
    const [scale, setScale] = React.useState('day');
    return (
      <section className="cv-w lw-gw" aria-labelledby="lw-ogt-h">
        <div className="lw-gwh"><W.WHead id="lw-ogt-h" title="График объекта" sub="этапы по фазам, работы и события объекта" /><W.ScaleTabs scale={scale} onScale={setScale} /></div>
        <GanttW g={gt} detail head="Фаза и этапы" scale={scale} onScale={setScale} onWork={onWork} onAddEvent={addEv} onRemoveEvent={rmEv} />
      </section>);
  }
  /* ───── окно объекта: тот же порядок, что на главной клиента, но глазами прораба ───── */
  function ObjectModal({ p, me, d, open, close, view }) {
    if (p.status === 'handoff') return <AcceptProject p={p} me={me} close={close} />;
    const h = P.views.clientHome(p), path = P.views.clientPhasePath(p), lv = P.views.clientLive(p);
    const o = FV.objects([p])[0];
    const q = FV.queue([p], t0()).filter((x) => x.kind !== 'handoff');
    const rev = FV.review([p]);
    const tm = FV.team([p]);
    const ch = FV.chats([p]);
    const gt = FV.gantt([p]);
    const feed = h.newWorks;
    const paidN = path.filter((x) => x.paid === 'done').length, payN = path.filter((x) => x.paid).length;
    return (
      <Modal size="full" className="lw-objm" sub={[p.client && p.client.name, p.client && p.client.phone, p.area ? p.area + ' м²' : ''].filter(Boolean).join(' · ')} title={p.title} onClose={close}
        head={<React.Fragment><button type="button" className="lw-btn-l" onClick={() => open('supply=' + p.id)}><Ic n="box" s={15} />Материалы</button><button type="button" className="lw-btn-l" onClick={() => open('spec=' + p.id)}><Ic n="calendar" s={15} />Спецификация</button><button type="button" className="lw-btn-l" onClick={() => open('docs=' + p.id)}><Ic n="doc" s={15} />Документы</button><button type="button" className="lw-btn-l" onClick={() => open('photos=' + p.id)}><Ic n="camera" s={15} />Фото</button>{(p.subOrders || []).some((o) => o.status !== 'cancelled') && <button type="button" className="lw-btn-l" onClick={() => open('subs=' + p.id)}><Ic n="tool" s={15} />Подрядчики</button>}<button type="button" className="lw-btn-l" onClick={() => open('chat=' + p.id + ':' + ((ch[0] && ch[0].id) || ''))}><Ic n="chat" s={15} />Чат объекта</button></React.Fragment>}>
        <FactsBand label="Ход ремонта" path={path}
          live={<React.Fragment>
            <span className="now">{lv && lv.now ? <React.Fragment><i className="cv-pulse" aria-hidden="true" />{lv.now.who ? <Face m={lv.now.who} size={28} /> : null}<span>Сейчас в работе: {lv.now.title}{lv.now.who ? ', ' + lv.now.who.name : ''}{lv.now.more ? ' и ещё ' + lv.now.more : ''}</span></React.Fragment> : 'Сейчас работ на объекте нет'}</span>
            {lv && lv.last && <span className="last">Последняя приёмка — {lv.last.daysAgo === 0 ? 'сегодня' : lv.last.daysAgo === 1 ? 'вчера' : lv.last.daysAgo + ' дн. назад'}, «{lv.last.title}»</span>}
          </React.Fragment>}
          facts={[
            { v: h.pct + ' %', lab: 'Принято ' + h.doneN + ' из ' + h.allN + ' работ' },
            { v: dateR(h.dueIso) || '—', lab: 'Сдача · ' + leftL(o.daysLeft), late: o.daysLeft < 0 },
            { v: paidN + ' из ' + payN + ' ' + plural(payN, 'фазы', 'фаз', 'фаз'), lab: 'Оплачено ' + rub(h.paid) + ' из ' + rub(h.total) },
            { v: o.review + ' на приёмке', lab: [o.inWork + ' в работе', o.returned ? o.returned + ' возврат' : '', o.late ? o.late + ' просрочено' : ''].filter(Boolean).join(' · ') },
          ]} />
        <RoomReady r={FV.roomSummary && FV.roomSummary(p)} open={open} />
        <NowCards rows={nowRows(q, open, 4, true).map((r) => (r.card ? r : { ...r, act: null }))} label="Сейчас важно на объекте" />
        <section className="cv-two">
          <CalendarW c={FV.calendar([p], t0())} title="Календарь объекта" onPick={pickEvent(open)} />
          <div className="cv-side"><AcceptW rows={rev} d={d} open={open} /><ChatsW chats={ch.map((c) => ({ ...c, when: when(c.at) }))} onPick={(c) => open('chat=' + c.pid + ':' + c.id)} empty="Переписки по объекту пока нет." /></div>
        </section>
        <ObjGantt gt={gt} onWork={(wk) => open(workHash({ pid: p.id, taskId: wk.id }))} />
        <W.MultiW groups={FV.multi(p, t0())} onPart={(g, x) => open(workHash({ pid: p.id, taskId: x.taskId }))} />
        {feed.length > 0 && <section className="lw-sec" aria-labelledby="lw-feed-h">
          <div className="lw-sech"><h2 className="h2" id="lw-feed-h">Принятые работы</h2><button type="button" className="cv-arrow-btn" onClick={() => open('photos=' + p.id)}>Фото, которые видит клиент<Ic n="ne" s={14} /></button></div>
          <div className="lw-feed">{feed.map((f, i) => (
            <button type="button" key={f.taskId} className="lw-fw" onClick={() => view(feed.map((x) => ({ kind: 'image', url: x.cover, name: x.title })), i)}>
              <span className="lw-fph"><img src={f.cover} alt="" loading="lazy" /><span className="tag pill-g">{f.photos} фото</span></span>
              <span className="t">{f.title}</span><span className="lab">{f.stage} · {f.date}</span>
            </button>))}</div>
        </section>}
        <TeamW t={tm} title="Бригада объекта" onPerson={(x) => open('team=' + x.id)} />
      </Modal>);
  }

  /* готовность ремонта по комнатам (KSH-504, решение владельца 03.11): только шаги, принятые прорабом, без дизайна, замеров и вех —
     то же число и та же подпись, что у клиента в «Реализации» (ForemanViews.roomSummary → repair); сданные ждут проверки — в приёмке.
     Проект без комнат — блока нет */
  function RoomReady({ r, open }) {
    if (!r || !r.rooms.length) return null;
    return (
      <section className="cv-w lw-rready" aria-labelledby="lw-rr-h">
        <div className="lw-sech"><div><h2 className="lw-h3" id="lw-rr-h">Готовность ремонта · {r.repair} %</h2><span className="lab">по работам, принятым прорабом</span></div>
          {r.pending ? <button type="button" className="cv-arrow-btn lw-rpend" onClick={() => open('accept=rooms')}>Сдано, ждёт проверки: {r.pending}<Ic n="ne" s={14} /></button> : null}</div>
        <div className="lw-rrg">{r.rooms.map((x) => (
          <span key={x.roomId}><span className="t" title={x.name}>{x.name}</span><b>{x.pct} %</b><span className="lw-wrbar" aria-hidden="true"><i style={{ width: x.pct + '%' }} /></span></span>))}</div>
      </section>);
  }

  /* новый объект: прораб видит состав, выбирает старт и бригаду и принимает (ProjectStore.accept — как в старом кабинете) */
  function AcceptProject({ p, me, close }) {
    const all = FV.team(FV.mine(me.id), t0()).people;
    const roles = Array.from(new Set(p.tasks.filter((t) => t.kind !== 'milestone').map((t) => t.role).filter((r) => r && !['foreman', 'pm', 'designer'].includes(r))));
    const fits = (x) => x.trades.some((tn) => roles.some((r) => P.roleName(r) === tn));
    const [pick, setPick] = React.useState(() => all.filter(fits).map((x) => x.id));
    const [start, setStart] = React.useState(isoAdd(t0(), 7));
    const [err, setErr] = React.useState('');
    const est = P.views.projectEstimate(p, 'client');
    const go = () => {
      try {
        const crew = all.filter((x) => pick.includes(x.id)).map((x) => { const s = St && St.get ? St.get(x.id) : null; return { accountId: x.id, name: x.name, role: (s && s.trades && s.trades[0]) || null, kind: 'staff' }; });
        P.accept(p.id, { foremanId: me.id, crew, startDate: start });
        close();
      } catch (e) { setErr(e.message); }
    };
    return (
      <Modal size="wide" className="lw-accp" sub={[p.client && p.client.name, p.area ? p.area + ' м²' : ''].filter(Boolean).join(' · ')} title={p.title} onClose={close}>
        <div className="lw-accp-g">
          <div>
            <div className="lw-accp-f">
              <span><b>{p.tasks.filter((t) => t.kind !== 'milestone').length}</b><span className="lab">работ в {p.stages.filter((s) => p.tasks.some((t) => t.stageId === s.id && t.kind !== 'milestone')).length} этапах</span></span>
              <span><b>{rub(p.totals.works || 0)}</b><span className="lab">работы по смете</span></span>
              <span><b>{rub(p.totals.materials || 0)}</b><span className="lab">материалы</span></span>
              <span><b>{Math.max(1, Math.round(((Date.parse(p.stages.map((s) => s.end).filter(Boolean).sort().pop() || p.startDate) - Date.parse(p.stages.map((s) => s.start).filter(Boolean).sort()[0] || p.startDate)) / 86400000) / 7))} нед.</b><span className="lab">срок по графику</span></span>
            </div>
            <h3 className="lw-h3">Что в смете</h3>
            <div className="cv-w lw-phlist">{(est ? est.phases : []).map((ph) => (
              <div key={ph.idx} className="lw-phr"><span className="no">/0{ph.idx}</span><span className="t">{ph.name}</span><span className="lab">{ph.stages.filter((s) => s.works.length).length} {plural(ph.stages.filter((s) => s.works.length).length, 'этап', 'этапа', 'этапов')}</span><span className="v">{rub(ph.sum || ph.total || 0)}</span></div>))}</div>
          </div>
          <div className="cv-w lw-accp-side">
            <h3 className="lw-h3">Принять объект</h3>
            <label className="lw-fld"><span className="lab">Старт работ</span><input type="date" value={start} min={t0()} onChange={(e) => setStart(e.target.value)} /></label>
            <div className="lab" style={{ marginTop: 18 }}>Бригада · по специальностям сметы отмечены подходящие</div>
            <div className="lw-crew">{all.map((x) => (
              <label key={x.id} className={'lw-cm' + (pick.includes(x.id) ? ' on' : '')}>
                <input type="checkbox" checked={pick.includes(x.id)} onChange={() => setPick(pick.includes(x.id) ? pick.filter((y) => y !== x.id) : pick.concat(x.id))} />
                <Face m={x} size={32} /><span className="tx"><span className="t">{x.name}</span><span className="lab">{x.trades.slice(0, 2).join(', ')} · загрузка {x.load} %</span></span>
              </label>))}</div>
            {err && <div className="lab" role="alert" style={{ color: 'var(--late)', marginTop: 10 }}>{err}</div>}
            <button type="button" className="btn-k lw-wide" onClick={go}>Принять объект в работу</button>
            <div className="lab" style={{ marginTop: 10 }}>Работы назначатся мастерам по специальности, график начнётся с выбранной даты.</div>
          </div>
        </div>
      </Modal>);
  }

  /* ───── окна разделов ───── */
  function ObjectsModal({ data, open, close }) {
    return (
      <Modal size="wide" title="Мои объекты" sub={data.objs.length + ' в работе' + (data.inc.length ? ' · ' + data.inc.length + ' ждёт приёма' : '')} onClose={close}>
        <div className="lw-tiles is-modal">{data.objs.map((o) => <ObjTile key={o.id} o={o} onOpen={() => open('obj=' + o.id)} />)}{data.inc.map((p) => <IncomingTile key={p.id} p={p} open={open} />)}</div>
      </Modal>);
  }
  /* график всех объектов (Роман, 07.11): одна вкладка — этапы, сгруппированные по фазам (фазы и этапы больше не переключаются);
     шкала — дни, недели или месяцы; блок «События на объекте» закреплён под шапкой: доставки подгруппой, сервис, визит клиента, свои события */
  function GanttModal({ data, open, close }) {
    const [scale, setScale] = React.useState('day');
    return (
      <Modal size="full" title="График работ" sub={'Все объекты · ' + dateR(data.gt.from, { year: 'numeric' }) + ' — ' + dateR(data.gt.to, { year: 'numeric' })} onClose={close}
        head={<W.ScaleTabs scale={scale} onScale={setScale} />}>
        <div className="lw-gleg"><span><i className="is-done" />сдано</span><span><i className="is-current" />идут работы</span><span><i className="is-late" />просрочено</span><span><i className="is-next" />впереди</span><span><i className="is-end" />сдача объекта</span>
          <span className="lw-gleg-ev"><Ic n="truck" s={13} />доставка</span><span className="lw-gleg-ev"><Ic n="user" s={13} />визит клиента</span><span className="lw-gleg-ev"><Ic n="trash" s={13} />вывоз мусора</span><span className="lw-gleg-ev"><Ic n="sofa" s={13} />сборка</span><span className="lw-gleg-ev"><Ic n="spark" s={13} />клининг</span><span className="lw-gleg-ev"><Ic n="calendar" s={13} />своё событие</span><span className="lw-gleg-ev is-pend"><i />пунктир — ждёт согласования клиента</span><span className="lw-gleg-ev"><i className="lw-gspm is-decide" />решить до</span><span className="lw-gleg-ev"><i className="lw-gspm is-order" />заказать до</span><span className="lw-gleg-ev lw-gthr">под угрозой — материал не успевает к этапу</span></div>
        <div className="cv-w lw-gw is-modal"><GanttW g={data.gt} detail scale={scale} onScale={setScale} onRow={(r) => open('obj=' + r.id)} onWork={(wk, r) => open(workHash({ pid: r.id, taskId: wk.id }))} onAddEvent={addEv} onRemoveEvent={rmEv} /></div>
      </Modal>);
  }
  /* календарь работ по дням (KSH-525): неделя по объектам или по людям, листается по неделям; завтра — списком по объектам */
  function WeekModal({ data, open, close }) {
    const [by, setBy] = React.useState('obj');
    const [from, setFrom] = React.useState(t0());
    const w = from === t0() ? data.wk : FV.week(data.list, from, t0());
    const n = data.wk.next;
    const tab = (k, l) => <button type="button" role="tab" aria-selected={by === k} className={by === k ? 'on' : ''} onClick={() => setBy(k)}>{l}</button>;
    return (
      <Modal size="full" title="Календарь работ" sub={'Все объекты · ' + dateR(w.from, { month: 'short' }) + ' — ' + dateR(w.to, { month: 'short', year: 'numeric' })} onClose={close}
        head={<div className="cv-ctabs lw-seg" role="tablist" aria-label="Строки">{tab('obj', 'По объектам')}{tab('people', 'По людям')}</div>}>
        <div className="lw-wknav">
          <button type="button" className="lw-btn-l" onClick={() => setFrom(isoAdd(w.from, -7))} aria-label="Прошлая неделя"><Ic n="left" s={14} /></button>
          <button type="button" className="lw-btn-l" disabled={from === t0()} onClick={() => setFrom(t0())}>Эта неделя</button>
          <button type="button" className="lw-btn-l" onClick={() => setFrom(isoAdd(w.from, 7))} aria-label="Следующая неделя"><Ic n="right" s={14} /></button>
          <span className="lab">Работа стоит в каждом дне от начала до срока. Охрой — работы без мастера, с мастером в отпуске и просроченные.</span>
        </div>
        <div className="cv-w lw-gw is-modal"><W.WeekW w={w} by={by} max={6} onRow={(r) => open(by === 'people' ? 'team=' + r.id : 'obj=' + r.pid)} onWork={(it) => open(workHash({ pid: it.pid, taskId: it.taskId }))} /></div>
        <h3 className="lw-h3" style={{ marginTop: 32 }}>Завтра, {W.nextLine(n).replace(/^./, (c) => c.toLowerCase())}</h3>
        <W.TomorrowW n={n} onObj={(r) => open('obj=' + r.pid)} onWork={(r, it) => open(workHash({ pid: r.pid, taskId: it.taskId }))} />
      </Modal>);
  }
  function TeamModal({ data, sel, me, open, close }) {
    const person = sel ? data.tm.people.find((x) => x.id === sel) : null;
    return (
      <Modal size="full" title="Команда" sub={data.tm.totals.people + ' ' + plural(data.tm.totals.people, 'человек', 'человека', 'человек') + teamSt(data.tm.totals) + ' · ' + data.objs.length + ' ' + plural(data.objs.length, 'объект', 'объекта', 'объектов')} onClose={close}>
        <div className={'lw-teamm' + (person ? ' has-sel' : '')}>
          <TeamW t={data.tm} full onPerson={(x) => open('team=' + x.id)} />
          {person && <PersonCard key={person.id} x={person} list={data.list} me={me} open={open} />}
        </div>
      </Modal>);
  }
  /* статус мастера в штате: на паузе — доводит свои работы, новых нарядов нет; ушёл из команды — его работы передать другому.
     Ставит и снимает руководитель в «Штате» Конструктора, прораб только видит */
  function AwayNote({ x }) {
    const st = x.st;
    if (!st && x.away && x.away.now) return (
      <div className="lw-swarn" role="note"><Ic n="pause" s={15} />
        <span><b>{W.awayLabel(x.away)}.</b> {x.away.note ? x.away.note.replace(/\.$/, '') + '. ' : ''}На эти дни наряд ему не выдать. Его работы на эти дни — в «Сейчас важно», передайте другому мастеру.</span></div>);
    if (!st) return null;
    const left = st.k === 'left', when = st.since ? ' ' + (left ? '' : 'с ') + dateR(st.since) : '';
    return (
      <div className={'lw-swarn' + (left ? ' is-late' : '')} role="note"><Ic n={left ? 'alert' : 'pause'} s={15} />
        <span><b>{left ? 'Ушёл из команды' + when : 'На паузе' + when}.</b> {st.note ? st.note.replace(/\.$/, '') + '. ' : ''}
          {left ? 'На нём ' + x.open + ' ' + plural(x.open, 'незакрытая работа', 'незакрытые работы', 'незакрытых работ') + ': откройте и передайте другому мастеру.'
            : 'Свои работы доводит, новые наряды ему не выдаются. Паузу снимает руководитель в «Штате».'}</span></div>);
  }
  /* отсутствия мастера (KSH-524): отпуск, больничный, отгул — период с датами, больничный можно без даты выхода.
     Штатному ставит прораб или руководитель заранее; на эти дни наряд ему не выдать, его работы в эти дни — «передать другому» */
  const AWK = [['vacation', 'Отпуск'], ['sick', 'Больничный'], ['dayoff', 'Отгул']];
  const dm = (d) => dateR(d, { month: 'short' });
  function AwayBlock({ x, me }) {
    const [form, setForm] = React.useState(null);
    const [err, setErr] = React.useState('');
    const Aw = St && St.away; if (!Aw) return null;
    const by = { role: (me && me.role) || 'foreman', name: (me && me.name) || '' };
    const now = t0();
    const rows = Aw.list(x.id).filter((a) => !a.to || a.to >= now);
    const act = (fn) => { try { fn(); setErr(''); return true; } catch (e) { setErr(e.message); return false; } };
    // «Вышел сегодня»: больничный кончился вчера (сегодня уже рабочий день); начался сегодня — записи нет
    const save = () => { if (act(() => Aw.add(x.id, { kind: form.kind, from: form.from, to: form.open ? '' : form.to, note: form.note }, by))) setForm(null); };
    const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
    return (
      <div className="lw-pco lw-away-b">
        <div className="lw-pco-h is-static">Отпуска и больничные</div>
        {rows.length === 0 && !form && <div className="lab">Ничего не запланировано.</div>}
        {rows.map((a) => (
          <div key={a.id} className="lw-pct lw-away-r"><span className="t">{cap(a.l)} · {a.from === a.to ? dm(a.from) : dm(a.from) + ' — ' + (a.to ? dm(a.to) : 'дата выхода не известна')}</span>
            <span className="lab">{[a.note, a.by && a.by.name ? 'внесено: ' + a.by.name : ''].filter(Boolean).join(' · ')}</span>
            <span className="lw-away-a">
              {a.kind === 'sick' && !a.to && <button type="button" className="lw-lnk lab" onClick={() => act(() => (a.from >= now ? Aw.remove(a.id, by) : Aw.update(a.id, { to: isoAdd(now, -1) }, by)))}>Вышел сегодня</button>}
              <button type="button" className="lw-lnk lab" onClick={() => act(() => Aw.remove(a.id, by))}>Убрать</button></span></div>))}
        {form ? (
          <div className="lw-away-f">
            <div className="lw-acc-flt" role="radiogroup" aria-label="Что ставим">{AWK.map((k) => <button type="button" key={k[0]} className={'lw-btn-l' + (form.kind === k[0] ? ' on' : '')} aria-pressed={form.kind === k[0]} onClick={() => set('kind', k[0])}>{k[1]}</button>)}</div>
            <div className="lw-away-d">
              <label className="lw-afld"><span className="lab">С</span><span className="in"><input type="date" value={form.from} onChange={(e) => set('from', e.target.value)} /></span></label>
              <label className="lw-afld"><span className="lab">По</span><span className="in"><input type="date" value={form.open ? '' : form.to} min={form.from} disabled={form.open} onChange={(e) => set('to', e.target.value)} /></span></label>
            </div>
            {form.kind === 'sick' && <label className="lw-away-o lab"><input type="checkbox" checked={!!form.open} onChange={(e) => set('open', e.target.checked)} />Дата выхода пока не известна</label>}
            <label className="lw-afld"><span className="lab">Комментарий, необязательно</span><span className="in"><input type="text" value={form.note} onChange={(e) => set('note', e.target.value)} placeholder="Например, согласовано в октябре" /></span></label>
            <span className="lw-wcrew"><button type="button" className="lw-btn-k" onClick={save}>Сохранить</button><button type="button" className="lw-lnk lab" onClick={() => { setForm(null); setErr(''); }}>Отмена</button></span>
          </div>)
          : <button type="button" className="cv-arrow-btn" onClick={() => setForm({ kind: 'vacation', from: isoAdd(now, 1), to: isoAdd(now, 14), open: false, note: '' })}>Поставить отпуск или больничный<Ic n="ne" s={14} /></button>}
        {err && <div className="lab is-late" role="alert">{err}</div>}
      </div>);
  }
  // карточка мастера: статус в штате, работы в руках по объектам, сданные за месяц (из чего выработка), ближайшее
  function PersonCard({ x, list, me, open }) {
    const mine = (p) => p.tasks.filter((t) => t.kind !== 'milestone' && t.assignees.some((a) => a.accountId === x.id));
    const m0 = t0().slice(0, 8) + '01';
    const ST = { progress: 'в работе', review: 'на приёмке', returned: 'возврат', todo: 'впереди' };
    return (
      <aside className="cv-w lw-pc" aria-label={'Мастер ' + x.name}>
        <div className="lw-pch"><FaceSt m={x} size={56} /><div><div className="t">{x.name}</div><div className="lab">{x.trades.join(', ')}</div></div></div>
        <AwayNote x={x} />
        <AwayBlock x={x} me={me} />
        <div className="lw-pcf"><span><b>{x.load} %</b><span className="lab">загрузка</span></span><span><b>{rub(x.outMonth)}</b><span className="lab">выработка за месяц</span></span><span><b>{x.doneMonth}</b><span className="lab">{plural(x.doneMonth, 'работа принята', 'работы приняты', 'работ принято')}</span></span></div>
        {list.map((p) => { const ts = mine(p).filter((t) => t.status !== 'done').sort((a, b) => String(a.start).localeCompare(String(b.start))); if (!ts.length) return null;
          return (
            <div key={p.id} className="lw-pco"><button type="button" className="lw-pco-h" onClick={() => open('obj=' + p.id)}>{short(p.title)}<Ic n="ne" s={14} /></button>
              {ts.slice(0, 6).map((t) => <button type="button" key={t.id} className={'lw-pct lw-btnrow is-' + t.status} onClick={() => open(workHash({ pid: p.id, taskId: t.id }))}><span className="t">{t.name}</span><span className="lab">{ST[t.status]} · {dateR(String(t.start).slice(0, 10), { month: 'short' })} — {dateR(String(t.end).slice(0, 10), { month: 'short' })}</span></button>)}
              {ts.length > 6 && <div className="lab">и ещё {ts.length - 6}</div>}
            </div>); })}
        {x.doneMonth > 0 && <div className="lw-pco"><div className="lw-pco-h is-static">Принято в этом месяце</div>
          {list.flatMap((p) => mine(p).filter((t) => t.status === 'done' && String(t.doneAt).slice(0, 10) >= m0).map((t) => ({ t, p }))).sort((a, b) => String(b.t.doneAt).localeCompare(String(a.t.doneAt))).slice(0, 8).map(({ t, p }) => (
            <div key={t.id} className="lw-pct is-done"><span className="t">{t.name}</span><span className="lab">{short(p.title)} · {dateR(String(t.doneAt).slice(0, 10), { month: 'short' })} · {rub((+t.price || 0) / (t.assignees.length || 1))}</span></div>))}
        </div>}
      </aside>);
  }
  /* шаги, сданные мастером по комнатам (KSH-504): решение — в карточке работы, блок «По комнатам» */
  function RoomQueue({ rows, open }) {
    return (
      <div className="cv-w lw-plain">{rows.length === 0 ? <div className="empty">Всё сданное по комнатам проверено.</div> : rows.map((x) => (
        <button type="button" key={x.pid + x.taskId} className="lw-pct lw-btnrow" onClick={() => open(workHash(x))}>
          <span className="t">{x.title} · {x.n} {plural(x.n, 'шаг', 'шага', 'шагов')}</span>
          <span className="lab">{[short(x.obj), x.stage, x.rooms.map((r) => r.name + (r.n > 1 ? ' · ' + r.n : '')).join(', ')].filter(Boolean).join(' · ')}</span></button>))}</div>);
  }
  function AcceptModal({ data, d, open, close, view, only }) {
    const ret = data.q.filter((x) => x.kind === 'returned');
    const rq = data.rq || [];
    const [flt, setFlt] = React.useState(only && rq.length ? 'rooms' : 'all');
    if (flt === 'rooms') return (
      <Modal size="wide" title="Приёмка работ" sub={data.rev.length + ' ждут решения · ' + ret.length + ' на доработке · по комнатам ждут проверки: ' + data.roomN} onClose={close}>
        <div className="lw-acc-flt" role="group" aria-label="Что показать"><button type="button" className="lw-btn-l" onClick={() => setFlt('all')}>Все</button><button type="button" className="lw-btn-l on" aria-pressed="true">Сдано по комнатам, ждёт проверки · {data.roomN}</button></div>
        <h3 className="lw-h3" style={{ marginTop: 24 }}>Сдано по комнатам</h3>
        <RoomQueue rows={rq} open={open} />
      </Modal>);
    return (
      <Modal size="wide" title="Приёмка работ" sub={data.rev.length + ' ждут решения · ' + ret.length + ' на доработке' + (data.roomN ? ' · по комнатам ждут проверки: ' + data.roomN : '')} onClose={close}>
        {rq.length > 0 && <div className="lw-acc-flt" role="group" aria-label="Что показать"><button type="button" className="lw-btn-l on" aria-pressed="true">Все</button><button type="button" className="lw-btn-l" onClick={() => setFlt('rooms')}>Сдано по комнатам, ждёт проверки · {data.roomN}</button></div>}
        {d.err && <div className="lab" role="alert" style={{ color: 'var(--late)', marginTop: 12 }}>{d.err}</div>}
        <div className="lw-acc-list">{data.rev.length === 0 ? <div className="empty">Все сданные работы приняты.</div> : data.rev.map((r) => (
          <div key={r.pid + r.taskId + (r.pointId || '')} className="cv-w lw-accard">
            <div className="hd"><div><div className="t">{r.title}</div><div className="lab">{[short(r.obj), r.stage, r.who ? 'сдал ' + r.who : '', r.at ? ago(r.at) : ''].filter(Boolean).join(' · ')}</div></div>
              <span className="lw-racts"><DecideActs r={r} d={d} open={open} /></span></div>
            {r.photos.length > 0 && <div className="lw-ph">{r.photos.map((x, i) => <button type="button" key={i} onClick={() => open(workHash(r, i))} aria-label={'Фото ' + (i + 1) + ': открыть и оставить замечание'}><img src={x.url} alt="" loading="lazy" /></button>)}</div>}
          </div>))}</div>
        {ret.length > 0 && <React.Fragment><h3 className="lw-h3" style={{ marginTop: 32 }}>На доработке</h3>
          <div className="cv-w lw-plain">{ret.map((x) => <button type="button" key={x.pid + x.taskId} className="lw-pct is-returned lw-btnrow" onClick={() => open(workHash(x))}><span className="t">{x.title}</span><span className="lab">{short(x.obj)} · {x.sub.replace('Вернули на доработку: ', '')}</span></button>)}</div></React.Fragment>}
        {rq.length > 0 && <React.Fragment><h3 className="lw-h3" style={{ marginTop: 32 }}>Сдано по комнатам</h3><RoomQueue rows={rq} open={open} /></React.Fragment>}
      </Modal>);
  }
  function CalendarModal({ data, open, close }) {
    const [tab, setTab] = React.useState('all');
    const [obj, setObj] = React.useState('all');
    const KINDS = [['all', 'Все'], ['work', 'Работы'], ['delivery', 'Доставки'], ['decide', 'Решения'], ['payment', 'Оплаты']];
    const to = isoAdd(t0(), 60);
    // повестка двух месяцев: просроченное и всё с сегодняшнего дня; вкладки — объект (по умолчанию все) и вид события
    const near = data.cal.events.filter((e) => e.status === 'overdue' || (e.date >= t0() && e.date <= to));
    const objs = calObjs(data.cal.events);
    const inObj = (e) => obj === 'all' || e.pid === obj;
    const list = near.filter((e) => (tab === 'all' || e.kind === tab) && inObj(e));
    return (
      <Modal size="wide" title="Календарь объектов" sub="Ближайшие два месяца по всем объектам" onClose={close}>
        {objs.length > 1 ? <CalTabs label="Объекты" kinds={[['all', 'Все объекты']].concat(objs.map((o) => [o.pid, short(o.obj)]))} tab={obj} setTab={setObj} counts={countBy(near, 'pid')} /> : null}
        <CalTabs kinds={KINDS} tab={tab} setTab={setTab} counts={countBy(near.filter(inObj), 'kind')} sub={objs.length > 1} />
        <div className="lw-calm">{list.length ? <CalAgenda list={list} t0={t0()} onPick={pickEvent(open)} /> : <div className="empty">Ничего не запланировано.</div>}</div>
      </Modal>);
  }
  /* чаты объектов: список слева, переписка справа; прораб пишет от себя (chatSend from: 'foreman') */
  const WHO = { client: 'Клиент', manager: 'Менеджер', foreman: 'Вы', designer: 'Дизайнер', procurement: 'Закупки', accountant: 'Бухгалтерия', executor: 'Мастер', super: 'Руководитель', marketing: 'Маркетолог' };
  function ChatModal({ data, sel, open, close }) {
    const [pid0, tid0] = (sel || '').split(':');
    const cur = data.chats.find((c) => c.pid === pid0 && (!tid0 || c.id === tid0)) || data.chats[0] || null;
    const [txt, setTxt] = React.useState('');
    const p = cur ? P.get(cur.pid) : null;
    const t = p && cur ? (p.chat.threads || []).find((x) => x.id === cur.id) : null;
    const send = () => { if (!txt.trim() || !t) return; P.chatSend(p.id, t.id, { from: 'foreman', t: txt.trim() }); setTxt(''); };
    return (
      <Modal size="wide" title="Чаты объектов" onClose={close}>
        <div className="lw-chat">
          <div className="lw-chl">{data.chats.map((c) => (
            <button type="button" key={c.pid + c.id} className={'lw-chr' + (cur && c.pid === cur.pid && c.id === cur.id ? ' on' : '') + (c.isNew ? ' is-new' : '')} onClick={() => open('chat=' + c.pid + ':' + c.id)}>
              <span className="t">{short(c.obj)}<span className="lab"> · {c.title}</span></span><span className="lab">{c.who}: {c.text}</span><span className="tm lab">{when(c.at)}</span></button>))}</div>
          <div className="lw-chv">{t ? <React.Fragment>
            <div className="lw-chh"><b>{p.title}</b><span className="lab">{t.members.filter((m) => m !== 'foreman').map((m) => WHO[m] || m).join(', ')}</span></div>
            <div className="lw-msgs cv-scroll">{t.msgs.map((m) => (
              <div key={m.id} className={'lw-msg' + (m.from === 'foreman' ? ' mine' : '')}><span className="lab">{WHO[m.from] || m.from} · {when(m.at)}</span>
                {m.t ? <span className="b">{m.t}</span> : null}{m.media && m.media.url ? <img src={m.media.url} alt={m.media.name} /> : null}</div>))}</div>
            <div className="lw-send"><input value={txt} onChange={(e) => setTxt(e.target.value)} placeholder="Написать в чат объекта" aria-label="Сообщение" onKeyDown={(e) => { if (e.key === 'Enter') send(); }} /><button type="button" className="lw-btn-k" disabled={!txt.trim()} onClick={send}>Отправить</button></div>
          </React.Fragment> : <div className="empty">Переписки пока нет.</div>}</div>
        </div>
      </Modal>);
  }

  /* карточка работы: принять или вернуть с замечаниями к фото; картинка к замечанию — files.js (в демо — только в этой вкладке) */
  function WorkModal({ arg, me, open, close, view }) {
    const [pid, taskId, pointId, at] = arg.split('~');
    const p = P.get(pid);
    const w = p ? FV.work(p, taskId, t0(), pointId || null) : null;
    if (!w || !W.WorkCard) return <Modal title="Работа не найдена" onClose={close}><div className="empty">Возможно, её уже убрали из сметы.</div></Modal>;
    const actor = { role: 'foreman', name: me.name };
    const act = (fn) => { try { fn(); close(); return null; } catch (e) { return { error: e.message }; } };
    const upload = (f) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind: 'remarks' }) : Promise.resolve({ url: URL.createObjectURL(f), name: f.name }));
    // наряд: ProjectStore.assign (replace | add | remove); ссылка — на задание в кабинете мастера, только у кого есть доступ (аккаунт не отозван)
    const Acc = window.AccessStore;
    const canLink = (id) => { const a = Acc && Acc.getAccount ? Acc.getAccount(id) : null; return !!a && a.status !== 'revoked' && a.status !== 'blocked'; };
    const onLink = (id) => { const tok = Acc.ensureToken(id); if (!tok) return null; const u = new URL('executor-task.html', window.location.href); u.searchParams.set('t', tok); u.searchParams.set('task', taskId); return u.toString(); };
    return <W.WorkCard key={arg} w={{ ...w, at: +at || 0 }} onClose={close} view={view} upload={upload}
      onAssign={(id, mode) => { try { P.assign(pid, taskId, id, mode); return null; } catch (e) { return { error: e.message }; } }}
      canLink={canLink} onLink={onLink} linkNote={DEMO ? 'В демо ссылка не откроется: объекты демо живут только в этой вкладке' : ''}
      onAccept={() => act(() => (pointId ? P.decideGate(pid, taskId, pointId, 'approve', '', actor) : P.setTaskStatus(pid, taskId, 'done', { actor })))}
      onReturn={({ remark, notes }) => act(() => (pointId ? P.decideGate(pid, taskId, pointId, 'return', remark, actor, notes) : P.setTaskStatus(pid, taskId, 'returned', { actor, remark, notes })))}
      onShift={(ok, note) => { try { P.decideShift(pid, taskId, ok, actor, note); return null; } catch (e) { return { error: e.message }; } }}
      onChat={() => open('chat=' + pid + ':')} onObject={() => open('obj=' + pid)}
      onRoomStep={(kind, roomId, stepId, comment) => {
        try {
          if (kind === 'close') { P.submitRoomStep(pid, taskId, roomId, stepId, actor); P.acceptRoomStep(pid, taskId, roomId, stepId, actor); }
          else if (kind === 'accept') P.acceptRoomStep(pid, taskId, roomId, stepId, actor);
          else P.returnRoomStep(pid, taskId, roomId, stepId, actor, comment);
          return null;
        } catch (e) { return { error: e.message }; }
      }} />;
  }

  /* материалы и доставки по объектам прораба; поставку принимает прораб — те же функции стора, что в старом кабинете */
  function SupplyModal({ data, sel, open, close, view }) {
    if (!W.SupplyW) return null;
    return <W.SupplyW s={FV.supply(data.list, t0())} t0={t0()} pid={sel} close={close} view={view}
      onAccept={(r) => open('delivery=' + r.pid + '~' + r.poId)} onObject={(pid) => open('obj=' + pid)} />;
  }
  /* спецификация одного объекта по этапам графика; только чтение — согласует клиент, заказывает закупщик, принимает прораб в «Материалах» */
  function SpecModal({ sel, me, open, close }) {
    const p = sel ? P.get(sel) : null;
    if (!W.SpecW || !p) return null;
    const actor = { role: 'foreman', name: me.name };
    const act = (fn) => { try { fn(); return {}; } catch (e) { return { error: e.message }; } };
    return <W.SpecW p={p} s={P.views.projectSpec(p, t0())} t0={t0()} close={close} onSupply={() => open('supply=' + p.id)}
      onDelivery={(ids, date) => act(() => P.createDeliveryFrom(p.id, ids, date, actor))} onEvent={(o) => act(() => P.addSpecEvents(p.id, o, actor))} />;
  }
  /* документы и акты: этап закрывает прораб (closeStage), пакет сдачи собирает он же — те же функции стора, что в старом кабинете */
  function DocsModal({ data, sel, me, open, close }) {
    if (!W.DocsW) return null;
    const actor = { role: 'foreman', name: me.name };
    const act = (fn) => { try { return fn() || {}; } catch (e) { return { error: e.message }; } };
    const upload = (pid, f) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind: 'handover' }) : Promise.resolve({ url: URL.createObjectURL(f), name: f.name }));
    return <W.DocsW s={FV.docs(data.list, t0())} t0={t0()} pid={sel} close={close} onObject={(pid) => open('obj=' + pid)} onChat={(pid) => open('chat=' + pid + ':')}
      onCloseStage={(r) => act(() => P.closeStage(r.pid, r.stageId, actor))}
      onUpload={(pid, kind, f) => upload(pid, f).then((u) => act(() => P.addHandoverDoc(pid, { kind, name: u.name || f.name, url: u.url }, actor)))}
      onRemove={(pid, id) => act(() => P.removeHandoverDoc(pid, id, actor))} />;
  }
  /* фото для клиента: к принятой работе без фото прораб добавляет снимки (addAttachment — клиент видит их сразу);
     сданные с фото он принимает или возвращает в карточке работы. В демо — data:-ссылка: blob живёт только во вкладке,
     и окна клиента такие фото не показывают; без связи с сервером (local) фото не прикладываем — клиент бы его не увидел */
  function PhotosModal({ data, sel, open, close, view }) {
    if (!W.PhotosW) return null;
    const read = (f) => new Promise((ok, no) => { const r = new FileReader(); r.onload = () => ok({ url: r.result, name: f.name }); r.onerror = () => no(new Error('Файл не прочитался')); r.readAsDataURL(f); });
    const upload = (pid, f) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind: 'photos' }) : read(f));
    const onUpload = (r, list) => list.reduce((pr, f) => pr.then(() => upload(r.pid, f)).then((u) => {
      if (u.local || /^blob:/.test(u.url || '')) throw new Error('Нет связи с сервером, фото не сохранилось. Попробуйте ещё раз');
      P.addAttachment(r.pid, r.taskId, { by: 'foreman', kind: /^video\//.test(f.type) ? 'video' : 'image', name: u.name || f.name, url: u.url });
    }), Promise.resolve()).then(() => ({}), (e) => ({ error: e.message || 'Фото не загрузилось' }));
    return <W.PhotosW s={FV.photos(data.list, t0())} t0={t0()} pid={sel} close={close} view={view} onUpload={onUpload}
      onWork={(r) => open(workHash(r))} onObject={(pid) => open('obj=' + pid)} />;
  }
  /* скрытые работы: снимок прораба (addHiddenComm) закрывает пункт с той же подписью и системой и уходит клиенту в пакет сдачи.
     Загрузка — как у «Фото для клиента»: в демо data:-ссылка, без связи с сервером фото не сохраняем */
  function HiddenModal({ data, sel, me, open, close, view }) {
    if (!W.HiddenW) return null;
    const actor = { role: 'foreman', name: me.name };
    const read = (f) => new Promise((ok, no) => { const r = new FileReader(); r.onload = () => ok({ url: r.result, name: f.name }); r.onerror = () => no(new Error('Файл не прочитался')); r.readAsDataURL(f); });
    const upload = (pid, f) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind: 'hidden-comms' }) : read(f));
    const onAdd = (spec, list) => list.reduce((pr, f) => pr.then(() => upload(spec.pid, f)).then((u) => {
      if (u.local || /^blob:/.test(u.url || '')) throw new Error('Нет связи с сервером, фото не сохранилось. Попробуйте ещё раз');
      P.addHiddenComm(spec.pid, { system: spec.system, room: spec.room || '', label: spec.label, url: u.url }, actor);
    }), Promise.resolve()).then(() => ({}), (e) => ({ error: e.message || 'Фото не загрузилось' }));
    return <W.HiddenW s={FV.hidden(data.list, t0())} pid={sel} close={close} view={view} onAdd={onAdd} onObject={(pid) => open('obj=' + pid)} />;
  }
  /* подрядчики: те же функции стора, что в старом кабинете (enterSubInvoice, orderSub, deliverSub, inviteSub, cancelSubOrder).
     Файлы счёта и поставки — как фото: в демо data:-ссылка, без связи с сервером не сохраняем */
  function SubsModal({ data, sel, me, open, close }) {
    if (!W.SubsW) return null;
    const actor = { role: 'foreman', name: me.name };
    const read = (f) => new Promise((ok, no) => { const r = new FileReader(); r.onload = () => ok({ url: r.result, name: f.name }); r.onerror = () => no(new Error('Файл не прочитался')); r.readAsDataURL(f); });
    const upload = (pid, f, kind) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind }) : read(f)).then((u) => {
      if (u.local || /^blob:/.test(u.url || '')) throw new Error('Нет связи с сервером, файл не сохранился. Попробуйте ещё раз');
      return { name: u.name || f.name, url: u.url, kind: /^image\//.test(f.type) ? 'image' : 'pdf' };
    });
    const run = (fn) => { try { fn(); return {}; } catch (e) { return { error: e.message } }; };
    const fail = (e) => ({ error: e.message || 'Не получилось сохранить' });
    const onInvoice = (o, cost, file) => (file ? upload(o.pid, file, 'sub-invoice') : Promise.resolve((o.invoice && o.invoice.file) || null))
      .then((fl) => { P.enterSubInvoice(o.pid, o.id, { cost, file: fl }, actor); return {}; }, fail).catch(fail);
    const onDeliver = (o, rep, files) => Promise.all(files.map((f) => upload(o.pid, f, 'sub-delivery')))
      .then((fl) => { P.deliverSub(o.pid, o.id, { ...rep, files: fl }, actor); return {}; }).catch(fail);
    const onLink = (o) => {
      try {
        const g = P.inviteSub(o.pid, o.id);
        const u = new URL('executor-task.html', window.location.href); u.searchParams.set('t', g.token);
        try { const pr = navigator.clipboard && navigator.clipboard.writeText(u.toString()); if (pr && pr.catch) pr.catch(() => {}); } catch (e) { /* ссылка видна в окне */ }
        return { url: u.toString() };
      } catch (e) { return { error: e.message }; }
    };
    return <W.SubsW s={FV.subs(data.list, t0())} pid={sel} close={close} onObject={(pid) => open('obj=' + pid)} onWork={(o) => open(workHash({ pid: o.pid, taskId: o.taskId }))}
      onInvoice={onInvoice} onDeliver={onDeliver} onLink={onLink} onOrder={(o) => run(() => P.orderSub(o.pid, o.id, actor))} onCancel={(o, why) => run(() => P.cancelSubOrder(o.pid, o.id, why, actor))} />;
  }
  /* цены мастеров на проект (KSH-518): прораб согласует в пределах цены компании; выше — видит, что решает менеджер или руководитель */
  function PricesModal({ data, sel, me, open, close }) {
    const [, bump] = React.useState(0);
    React.useEffect(() => {
      const on = () => bump((x) => x + 1);
      ['obitel:exec-prices', 'obitel:sync', 'storage'].forEach((e) => window.addEventListener(e, on));
      return () => ['obitel:exec-prices', 'obitel:sync', 'storage'].forEach((e) => window.removeEventListener(e, on));
    }, []);
    const [why, setWhy] = React.useState({});
    const [rej, setRej] = React.useState(null);
    const [err, setErr] = React.useState('');
    const EP = window.ExecPrices;
    const all = FV.prices ? FV.prices(data.list) : [];
    const rows = sel ? all.filter((o) => o.pid === sel) : all;
    const key = (o) => o.pid + '~' + o.executorId + '~' + o.nodeId;
    const ERR = { rights: 'Решать может прораб этого объекта', level: 'Цена выше цены компании — решает менеджер или руководитель', reason: 'Напишите причину отказа', state: 'Предложение уже решено или отозвано' };
    const decide = (o, d) => {
      const r = EP.decideOffer(o.executorId, o.pid, o.nodeId, d, { by: me.name, role: 'foreman', reason: why[key(o)] || '' });
      setErr(r.ok ? '' : ERR[r.error] || 'Не получилось');
      if (r.ok) setRej(null);
    };
    const mine = rows.filter((o) => o.level === 'foreman'), up = rows.filter((o) => o.level !== 'foreman');
    const qty = (o) => (o.qty ? (Math.round(o.qty * 100) / 100).toLocaleString('ru-RU') + (o.unit ? ' ' + o.unit : '') : '');
    const diff = (o) => { if (!(o.norm > 0)) return null; const v = Math.round((o.price / o.norm - 1) * 100); return v === 0 ? 'как у компании' : (v > 0 ? '+' : '−') + Math.abs(v) + ' % к цене компании'; };
    // карточка решения — как «Согласования» клиента: название, строка «кто · объём», пары «подпись — значение», кнопки внизу; функцией — поле причины не теряет фокус
    const card = (o, ro) => (
      <div key={key(o)} className={'card cv-dec lw-pr-card' + (ro ? ' is-ro' : '')}>
        <div className="cv-chead"><div style={{ minWidth: 0 }}><div className="t">{o.name}</div><div className="lab" style={{ marginTop: 3 }}>{[sel ? '' : short(o.obj), o.executorName, qty(o)].filter(Boolean).join(' · ')}</div></div></div>
        <div className="lw-pr-price">{rub(o.price)}<span className="lab">{diff(o) || 'новая цена'}</span></div>
        <dl className="cv-meta"><div><dt>Сейчас</dt><dd>{rub(o.current)}</dd></div><div><dt>Цена компании</dt><dd>{o.norm != null ? rub(o.norm) : '—'}</dd></div></dl>
        {ro ? <div className="lab" style={{ marginTop: 14 }}>Решает {o.levelL}</div> : (
          <div style={{ marginTop: 14 }}>
            {rej === key(o) && <textarea className="cv-ta" rows={2} autoFocus placeholder="Почему отказ — мастер увидит причину" aria-label={'Причина отказа: ' + o.name} value={why[key(o)] || ''} onChange={(e) => setWhy({ ...why, [key(o)]: e.target.value })} onKeyDown={(e) => { if (e.key === 'Escape') setRej(null); }} />}
            <div className="cv-dbtn" style={{ display: 'flex', gap: 8, marginTop: rej === key(o) ? 10 : 0 }}>
              {rej === key(o)
                ? <React.Fragment><button type="button" className="btn-l" onClick={() => setRej(null)}>Отмена</button><button type="button" className="btn-k" disabled={!(why[key(o)] || '').trim()} onClick={() => decide(o, 'reject')}>Отправить отказ</button></React.Fragment>
                : <React.Fragment><button type="button" className="btn-l" onClick={() => setRej(key(o))}>Отказать</button><button type="button" className="btn-k" onClick={() => decide(o, 'approve')}>Согласовать</button></React.Fragment>}
            </div>
          </div>)}
      </div>);
    const n = mine.length;
    return (
      <Modal size="wide" title="Цены мастеров" sub={n ? n + ' ' + (n === 1 ? 'ждёт' : 'ждут') + ' вашего решения' + (sel ? ' · ' + short((data.list.find((p) => p.id === sel) || {}).title || '') : '') : 'от вас сейчас ничего не нужно'} onClose={close}>
        <p className="cv-lede" style={{ marginTop: 16, fontSize: 16 }}>Мастер предложил свою цену на работу в этом проекте. После «Согласовать» по ней считается его сумма. Цена клиента и смета не меняются.</p>
        {err && <div className="att" role="alert" style={{ marginTop: 12 }}><Ic n="alert" s={16} /><span>{err}</span></div>}
        {!rows.length && <div className="lab" style={{ marginTop: 20 }}>Новых цен от мастеров нет.</div>}
        <div className="cv-apr">
          {n > 0 && <section><h2 className="h2" style={{ fontSize: 20 }}>Ваше решение</h2><div className="cv-cards" style={{ marginTop: 14 }}>{mine.map((o) => card(o, false))}</div></section>}
          {up.length > 0 && <section><h2 className="h2" style={{ fontSize: 20 }}>Выше цены компании</h2><div className="lab" style={{ marginTop: 6 }}>Такие цены согласует менеджер или руководитель. Здесь — чтобы вы знали, о чём просит мастер.</div><div className="cv-cards" style={{ marginTop: 14 }}>{up.map((o) => card(o, true))}</div></section>}
        </div>
      </Modal>);
  }
  function DeliveryModal({ arg, me, open, close, view }) {
    const [pid, poId] = arg.split('~');
    const p = P.get(pid);
    const [d] = React.useState(() => (p ? FV.delivery(p, poId) : null)); // снимок до приёмки: после неё заказ закрыт, а окно показывает итог
    if (!d || !W.DeliveryAccept) return <Modal title="Поставка не найдена" onClose={close}><div className="empty">Возможно, заказ отменили или его уже приняли.</div></Modal>;
    if (d.closed) return <Modal title={'Поставка ' + d.poId + ' уже принята'} sub={short(d.obj)} onClose={close}><div className="empty">Её позиции — в разделе «Материалы и доставки».</div></Modal>;
    const actor = { role: 'foreman', name: me.name };
    const act = (fn) => { try { return fn() || {}; } catch (e) { return { error: e.message }; } };
    const upload = (f) => (!DEMO && window.Files ? window.Files.upload(f, { pid, kind: 'delivery-' + poId }) : Promise.resolve({ url: URL.createObjectURL(f), name: f.name }));
    return <W.DeliveryAccept key={arg} d={d} t0={t0()} onClose={close} view={view} upload={upload} onObject={() => open('obj=' + pid)}
      onAccept={(report) => act(() => P.acceptDelivery(pid, poId, report, actor))}
      onReturn={(spec) => act(() => P.createReturn(pid, poId, spec, actor))} />;
  }

  function App() {
    useStore();
    const me = useMe();
    const [modal, open, close] = useHash();
    const [lb, setLb] = React.useState(null);
    const view = (items, at) => setLb({ items, at });
    const d = useDecide(me);
    if (!P || !FV) return <div className="cv-page"><div className="empty">Кабинет не загрузился. Обновите страницу.</div></div>;
    const list = FV.mine(me.id), inc = FV.incoming(me.id);
    const now = t0();
    const objs = FV.objects(list, now), q = FV.queue(list, now, me.id), tm = FV.team(list, now), rev = FV.review(list);
    const rq = FV.roomQueue ? FV.roomQueue(list) : [];
    const data = { wk: FV.week(list, now, now), wk7: FV.week(list, now, now, { rolling: true }), list, objs, inc, q, tm, rev, rq, roomN: rq.reduce((a, x) => a + x.n, 0), cal: FV.calendar(list, now), gt: FV.gantt(list, now), chats: FV.chats(list), sum: FV.summary(objs, tm, q) };
    const arg = (k) => (modal.indexOf(k + '=') === 0 ? modal.slice(k.length + 1) : null);
    const objId = arg('obj'), objP = objId ? P.get(objId) : null;
    return (
      <React.Fragment>
        <Home me={me} data={data} open={open} view={view} d={d} />
        {modal === 'objects' && <ObjectsModal data={data} open={open} close={close} />}
        {objP && <ObjectModal key={objP.id} p={objP} me={me} d={d} open={open} close={close} view={view} />}
        {modal === 'gantt' && <GanttModal data={data} open={open} close={close} />}
        {modal === 'week' && <WeekModal data={data} open={open} close={close} />}
        {(modal === 'team' || arg('team')) && <TeamModal data={data} sel={arg('team')} me={me} open={open} close={close} />}
        {(modal === 'accept' || arg('accept') === 'rooms') && <AcceptModal key={modal} data={data} d={d} open={open} close={close} view={view} only={arg('accept') === 'rooms'} />}
        {modal === 'calendar' && <CalendarModal data={data} open={open} close={close} />}
        {(modal === 'chat' || arg('chat') != null) && <ChatModal data={data} sel={arg('chat')} open={open} close={close} />}
        {arg('work') && <WorkModal arg={arg('work')} me={me} open={open} close={close} view={view} />}
        {(modal === 'supply' || arg('supply')) && <SupplyModal data={data} sel={arg('supply')} open={open} close={close} view={view} />}
        {arg('spec') && <SpecModal sel={arg('spec')} me={me} open={open} close={close} />}
        {(modal === 'docs' || arg('docs')) && <DocsModal data={data} sel={arg('docs')} me={me} open={open} close={close} />}
        {(modal === 'hidden' || arg('hidden')) && <HiddenModal data={data} sel={arg('hidden')} me={me} open={open} close={close} view={view} />}
        {(modal === 'subs' || arg('subs')) && <SubsModal data={data} sel={arg('subs')} me={me} open={open} close={close} />}
        {(modal === 'prices' || arg('prices')) && <PricesModal data={data} sel={arg('prices')} me={me} open={open} close={close} />}
        {(modal === 'photos' || arg('photos')) && <PhotosModal data={data} sel={arg('photos')} open={open} close={close} view={view} />}
        {arg('delivery') && <DeliveryModal key={arg('delivery')} arg={arg('delivery')} me={me} open={open} close={close} view={view} />}
        {lb && Lightbox && <Lightbox items={lb.items} at={lb.at} onClose={() => setLb(null)} />}
      </React.Fragment>);
  }

  window.ForemanV2 = { App };
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
})();
