/* marketing-v2-app.jsx — кабинет маркетолога в виде кабинета клиента (KSH-459): шапка, главная со сводкой, разделы — окнами по #hash.
   Собран из библиотеки виджетов lk-widgets.jsx (window.LKW) на данных marketing-views.js (window.MarketingViews).
   Главная: сводка (окна на неделе, выезды, закрывающиеся окна, посты, согласие клиентов), «Сейчас важно», ближайшие выезды, идеи на 30 дней,
   чаты, свежие материалы, «Мои задачи» (общий таск-менеджер: те же решения, что в прежнем «Обзоре», плюс посты и согласие).
   Окна: #windows[=<состояние>] — работы для съёмки карточками (без горизонтальной прокрутки): бронь (requestShoot), «Убрать из SMM» /
   «Добавить вручную» (setSmmManual); #win=<pid>~<taskId> — одна работа с формой брони; #shoots, #shoot=<pid>~<id> — выезд: подтвердить
   перенос прораба (confirmShoot), «Завершить и загрузить» (completeShoot), перенести (moveShoot), отменить (cancelShoot);
   #plan[=<вкладка>] — контент-план: идеи на 30 дней с «Показать все» и отметкой согласия, посты по статусам (ContentPlan);
   #post=<id> — пост: поля, медиа со съёмки и из медиатеки, переходы статусов, ссылки публикаций; #media — медиатека: скачать, выбрать
   несколько «в пост», «Показать клиенту», «Для сделок», файлы без превью скрываются; #consent — согласие клиентов и «Запросить согласие»
   (requestMarketingConsent); #chat[=project:<pid>:<tid>]. «Обсудить» открывает тред задачи (chatOpenForTask) в окне чатов. */
(function () {
  'use strict';
  const MV = window.MarketingViews, W = window.LKW, A = window.AccessStore;
  const P = window.ProjectStore;
  const { Ic, Header, Modal, FactsBand, NowCards, RowsW, ChatsW, plural, cap, dateR, short } = W;
  const t0 = () => new Date().toISOString().slice(0, 10);

  function useMe() {
    const acc = A && A.currentAccount ? A.currentAccount() : null;
    const ac = W.access(acc, ['marketing', 'super']);
    const home = ac.zone('dash'), wins = ac.zone('windows'), shoots = ac.zone('shoots'), plan = ac.zone('plan'), media = ac.zone('media'), chat = ac.zone('chat');
    return { acc, name: (acc && acc.name) || 'Маркетолог', home, wins, shoots, plan, media, chat,
      deny: ac.deny || (home || wins || shoots || plan || media || chat ? '' : 'Кабинет маркетолога не входит в выданный доступ.') };
  }
  const actor = (me) => ({ role: 'marketing', name: me.name });
  const EVS = ['storage', 'obitel:sync'];
  function useStore() {
    const [, tick] = React.useState(0);
    React.useEffect(() => (P ? P.subscribe(() => tick((x) => x + 1)) : undefined), []);
    React.useEffect(() => (window.ContentPlan ? window.ContentPlan.subscribe(() => tick((x) => x + 1)) : undefined), []);
    React.useEffect(() => { const on = () => tick((x) => x + 1); EVS.forEach((e) => window.addEventListener(e, on)); return () => EVS.forEach((e) => window.removeEventListener(e, 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' }));
  // год dateR добавляет сам, если дата не в текущем году (аудит 22.10)
  const dr = (d) => (d ? dateR(String(d).slice(0, 10), { month: 'short' }) : '');
  const drr = (a, b) => (a === b ? dr(a) : dr(a) + ' — ' + dr(b));
  const when = (iso) => { if (!iso) return ''; const d = String(iso).slice(0, 10); return d === t0() ? new Date(iso).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) : dr(d); };
  const go = (url) => { window.location.href = url; };
  const FORMATS = () => (window.ConstructorData && window.ConstructorData.SMM_FORMATS) || [];
  const fmtL = (id) => (FORMATS().find((f) => f.id === id) || { label: id }).label;

  const NAV = [['', 'Главная'], ['windows', 'Окна съёмок'], ['shoots', 'Выезды'], ['plan', 'Контент-план'], ['media', 'Медиатека'], ['chat', 'Чаты']];
  const MORE = [['consent', 'Согласие клиентов'], ['tools.html?role=marketing', 'Инструменты'], ['marketing-profile.html', 'Мой профиль']];
  const CONS_CLS = { none: 'pill-d', anonymous: 'pill-n', public: 'pill-g' };
  const WIN_CLS = { future: 'pill-d', open: 'pill-g', closing: 'pill-n cv-over', closed: 'pill-d' };
  const SHOOT_CLS = { requested: 'pill-n', confirmed: 'pill-g', moved: 'pill-n cv-over', done: 'pill-d', canceled: 'pill-d' };
  const POST_CLS = { idea: 'pill-d', shoot: 'pill-n', edit: 'pill-n', review: 'pill-n', ready: 'pill-g', published: 'pill-d' };
  const MEDIA_SRC = { shoot: 'выезд', task: 'сдача работы', chat: 'чат / загрузка' };
  const err = (e) => (e ? <div className="att" role="alert" style={{ marginTop: 12 }}><Ic n="alert" s={16} /><span>{e}</span></div> : null);
  const okNote = (t) => (t ? <div className="note-s" role="status" style={{ marginTop: 12 }}><Ic n="check" c="#2F5D3A" s={16} /><span>{t}</span></div> : null);
  function useRun() {
    const [e, setE] = React.useState(''); const [ok, setOk] = React.useState('');
    const run = (fn, msg) => { try { fn(); setE(''); setOk(msg || ''); return true; } catch (x) { setE(x.message); setOk(''); return false; } };
    return [run, e, ok];
  }
  const Cons = ({ c }) => <span className={CONS_CLS[c] || 'pill-d'} title={'Согласие клиента: ' + ((P.CONSENT_LABEL || {})[c] || '')}>{MV.CONSENT_L[c] || MV.CONSENT_L.none}</span>;
  const ShootPill = ({ s }) => <span className={SHOOT_CLS[s.status] || 'pill-d'}>{s.status === 'moved' ? (s.movedBy === 'foreman' ? 'Прораб перенёс — подтвердите' : 'Ждёт прораба') : MV.SHOOT_L[s.status]}</span>;
  // «Обсудить»: тред задачи (маркетолог, прораб, исполнитель) — открывается в окне чатов
  const discuss = (me, pid, taskId, open, run) => run(() => { const th = P.chatOpenForTask(pid, taskId, actor(me)); if (th) open('chat=project:' + pid + ':' + th.id); });

  /* «Мои задачи»: строки маркетолога (MarketingViews.tasks) + задачи роли из общего таск-менеджера */
  function myInbox(d, open) {
    const T = window.TaskViews;
    const order = (r) => (r.late ? 0 : r.now ? 1 : 2);
    const own = MV.tasks(d).map((r) => Object.assign({}, r, { act: () => open(r.hash) }));
    const inbox = T && T.inbox ? T.inbox('marketing', 'marketing', t0()) : [];
    return own.concat(inbox).sort((a, b) => order(a) - order(b) || String(a.due || '9999').localeCompare(String(b.due || '9999')));
  }

  /* ───── главная ───── */
  function Home({ me, d, open }) {
    const { f } = d;
    const T = window.TaskViews;
    const first = me.acc && me.acc.name ? me.acc.name.split(' ')[0] : '';
    const tasks = MV.tasks(d);
    const mine = tasks.filter((r) => r.next.who === 'me');
    const now = tasks.filter((r) => r.late || r.now).slice(0, 3).map((r) => ({ key: r.id, kindL: r.kindL, late: !!r.late, title: r.title, sub: [r.obj ? short(r.obj) : '', r.sub].filter(Boolean).join(' · '),
      act: { text: r.next.l, main: r.next.who === 'me', onClick: () => open(r.hash) } }));
    const wk = W.isoAdd ? W.isoAdd(t0(), 7) : t0();
    const next = d.shoots.filter((s) => MV.LIVE.includes(s.status) && s.date >= t0()).slice(0, 6);
    const ideas = MV.ideas(false);
    const fresh = d.media.filter((m) => m.kind === 'image').slice(0, 4);
    const inbox = myInbox(d, open);
    return (
      <div className="cv-page lw-page">
        <Header nav={NAV} more={MORE} cur="" right={<span className="cv-ava" title={me.name}>{me.name.split(' ').map((w) => w[0]).join('').slice(0, 2)}</span>} />
        <main id="main" tabIndex={-1} className="cv-main">
          <section className="cv-hero">
            <div><div className="lab">{todayLine()} · {d.consent.length} {plural(d.consent.length, 'объект', 'объекта', 'объектов')}</div><h1 className="cv-hello">{greeting()}{first ? ', ' + first : ''}</h1></div>
            <div className="cv-hero-act">
              {mine.length ? <button type="button" className="btn-k" onClick={() => open(mine[0].hash)}>Ждут вас · {mine.length}</button>
                : <span className="pill-g"><span className="dot" />Решений и срочных окон нет</span>}
            </div>
          </section>

          <FactsBand label="Сводка SMM"
            live={<span className="now">{f.closing ? <React.Fragment><i className="cv-pulse" aria-hidden="true" /><span>{f.closing} {plural(f.closing, 'окно закрывается', 'окна закрываются', 'окон закрываются')} в ближайшие 2 дня</span></React.Fragment> : <span>Закрывающихся окон нет</span>}</span>}
            facts={[
              { v: f.week + ' ' + plural(f.week, 'работа', 'работы', 'работ'), lab: 'Можно снять на неделе', onClick: () => open('windows') },
              { v: f.upcoming + ' ' + plural(f.upcoming, 'выезд', 'выезда', 'выездов'), lab: f.waitForeman ? f.waitForeman + ' ждут прораба' : f.toConfirm ? f.toConfirm + ' ждут вас' : 'Всё согласовано', onClick: () => open('shoots'), late: f.toConfirm > 0 || f.toFinish > 0 },
              { v: f.closing + ' закрываются', lab: 'Окно ≤ 2 дней', onClick: () => open('windows=closing'), late: f.closing > 0 },
              { v: f.inWork + ' ' + plural(f.inWork, 'пост', 'поста', 'постов'), lab: [f.review ? f.review + ' на проверке' : '', f.ready ? f.ready + ' к публикации' : '', f.returned ? f.returned + ' вернули' : ''].filter(Boolean).join(' · ') || 'В работе', onClick: () => open('plan'), late: f.returned > 0 },
              { v: (f.consent.public + f.consent.anonymous) + ' из ' + d.consent.length, lab: 'С согласием клиента' + (f.asked ? ' · ' + f.asked + ' запрошено' : ''), onClick: () => open('consent') },
            ]} />

          <NowCards rows={now} />

          <section className="cv-two lw-two">
            {me.shoots ? <RowsW id="mk-sh-h" title="Ближайшие выезды" n={next.filter((s) => s.date <= wk).length} className="is-grow" text={'Все' + (d.shoots.length > 6 ? ' · ' + d.shoots.length : '')} onOpen={() => open('shoots')}
              rows={next.map((s) => ({ key: s.pid + s.id, ic: 'camera', late: s.status === 'moved' && s.movedBy === 'foreman', title: s.tasks.join(', '), sub: short(s.obj) + ' · ' + dr(s.date) + (s.time ? ', ' + s.time : ''), right: s.status === 'moved' ? (s.movedBy === 'foreman' ? 'Подтвердите' : 'Ждёт прораба') : MV.SHOOT_L[s.status], onClick: () => open('shoot=' + s.pid + '~' + s.id) }))}
              empty="Выездов впереди нет — забронируйте дату в «Окнах съёмок»." /> : <div />}
            <div className="cv-side">
              {me.plan && <RowsW id="mk-id-h" title={'Идеи на ' + MV.HORIZON + ' дней'} n={ideas.shown.length} max={300} text="Контент-план" onOpen={() => open('plan')}
                rows={ideas.shown.slice(0, 5).map((i) => ({ key: i.key, ic: 'spark', title: i.name, sub: short(i.obj) + ' · ' + dr(i.date) + (i.consent === 'none' ? ' · только архив' : ''), right: MV.CONSENT_L[i.consent], onClick: () => open('plan') }))}
                empty="Идей на месяц нет — они появятся из плана работ." />}
              {me.chat && <ChatsW chats={d.chats.map((c) => Object.assign({}, c, { when: when(c.at) }))} onOpen={() => open('chat')} onPick={(c) => open('chat=project:' + c.pid + ':' + c.id)} />}
            </div>
          </section>

          {me.media && fresh.length > 0 && <section className="lw-sec" aria-labelledby="mk-md-h">
            <div className="lw-sech"><h2 className="h2" id="mk-md-h">Свежие материалы</h2><button type="button" className="cv-arrow-btn" onClick={() => open('media')}>Медиатека · {d.media.length}<Ic n="ne" s={14} /></button></div>
            <div className="mk-fresh">{fresh.map((m) => (
              <button type="button" key={m.url} className="mk-fr" onClick={() => open('media')} aria-label={'Медиатека: ' + (m.taskName || m.name)}>
                <img src={m.url} alt="" loading="lazy" onError={(e) => { e.currentTarget.parentNode.style.display = 'none'; }} />
                <span className="tx"><span className="t">{m.taskName || m.name || 'Без названия'}</span><span className="lab">{short(m.obj)} · {MEDIA_SRC[m.source]}</span></span>
              </button>))}</div>
          </section>}

          {T && T.inbox && <section id="mk-tasks" className="cv-w lw-gw lw-scope" aria-label="Мои задачи">
            <W.TaskList title="Мои задачи" t0={t0()} onOpen={(r) => (r.act ? r.act() : r.hash ? open(r.hash) : r.href ? go(r.href) : null)}
              tabs={[
                { id: 'in', label: 'Мне сделать', rows: inbox, empty: 'Решений и закрывающихся окон нет.', boardRows: inbox.concat(T.inboxDone ? T.inboxDone('marketing', t0()) : []), columns: T.IN_COLUMNS, colOf: T.inCol },
              ].concat(T.outbox ? [{ id: 'out', label: 'Отправлено мной', rows: T.outbox({ role: 'marketing' }, t0()), empty: 'Вы ничего не возвращали на доработку.',
                buckets: T.OUT_BUCKETS, bucketOf: T.outBucket, boardRows: T.outbox({ role: 'marketing' }, t0(), { closed: true }), columns: T.OUT_COLUMNS, colOf: T.outCol }] : [])} />
          </section>}
        </main>
      </div>);
  }

  /* ───── окна съёмок: карточки вместо широкой таблицы ───── */
  function BookForm({ r, me, done }) {
    const w = r.window; const min = w.from > t0() ? w.from : t0();
    const [date, setDate] = React.useState(min <= w.to ? min : w.to);
    const [time, setTime] = React.useState('10:00');
    const [note, setNote] = React.useState('');
    const [run, e] = useRun();
    return (
      <div className="mk-book">
        <div className="lab">Окно съёмки: <b>{drr(w.from, w.to)}</b>. Прораб подтвердит или предложит другое время.</div>
        <div className="pz-slot">
          <label><span className="lab">Дата</span><input className="pz-in" type="date" value={date} min={min} max={w.to} onChange={(x) => setDate(x.target.value)} /></label>
          <label><span className="lab">Время</span><input className="pz-in" type="time" value={time} onChange={(x) => setTime(x.target.value)} /></label>
          <label style={{ gridColumn: 'span 2' }}><span className="lab">Что хотим снять</span><input className="pz-in" value={note} placeholder="Напр. кадр с подсветкой ниши" onChange={(x) => setNote(x.target.value)} /></label>
        </div>
        {r.prep.length > 0 && <div className="lab" style={{ marginTop: 8 }}>Подготовят к съёмке: {r.prep.join(' · ')}</div>}
        {err(e)}
        <div className="dz-acts"><button type="button" className="btn-l" onClick={() => done(false)}>Отмена</button>
          <button type="button" className="btn-k" onClick={() => { if (run(() => P.requestShoot(r.pid, [r.taskId], date, time, note.trim(), actor(me)))) done(true); }}>Отправить прорабу</button></div>
      </div>);
  }
  function WinCard({ r, me, open, book, setBook }) {
    const [run, e] = useRun();
    const live = !!r.shoot && MV.LIVE.includes(r.shoot.status);
    const remove = () => { if (window.confirm('Убрать «' + r.name + '» из SMM? Вернуть можно внизу окна.')) run(() => P.setSmmManual(r.pid, r.taskId, false, actor(me))); };
    return (
      <div className={'card mk-card' + (r.window.state === 'closing' ? ' is-over' : '')}>
        <div className="cv-chead"><div style={{ minWidth: 0 }}><div className="t">{r.name}</div><div className="lab" style={{ marginTop: 3 }}>{[r.obj, r.stage, r.parent].filter(Boolean).join(' · ')}</div></div><span className={WIN_CLS[r.window.state]}>{MV.WIN_L[r.window.state]}</span></div>
        <dl className="cv-meta mk-meta">
          <div><dt>Окно</dt><dd>{drr(r.window.from, r.window.to)}</dd></div>
          <div><dt>Согласие</dt><dd><Cons c={r.consent} /></dd></div>
          <div><dt>Формат</dt><dd>{r.formats.length ? r.formats.map(fmtL).join(', ') : '—'}</dd></div>
          <div><dt>Источник</dt><dd>{r.source === 'designer' ? '★ дизайнер' : r.source === 'manual' ? 'вручную' : 'каталог'}{r.star && r.star.note ? ' · ' + r.star.note : ''}</dd></div>
          <div><dt>Выезд</dt><dd>{r.shoot ? <React.Fragment><ShootPill s={r.shoot} /> <span className="lab">{dr(r.shoot.date)}{r.shoot.time ? ', ' + r.shoot.time : ''}</span></React.Fragment> : 'нет'}</dd></div>
        </dl>
        {r.address && r.address !== r.obj && <div className="lab" style={{ marginTop: 6 }}>{r.address}</div>}
        {err(e)}
        {book ? <BookForm r={r} me={me} done={() => setBook(null)} /> : (
          <div className="dz-acts">
            {r.window.state !== 'closed' && !live && <button type="button" className="btn-k" onClick={() => setBook(r.pid + '~' + r.taskId)}>Забронировать выезд</button>}
            {live && <button type="button" className="btn-l" onClick={() => open('shoot=' + r.pid + '~' + r.shoot.id)}>Открыть выезд</button>}
            <button type="button" className="btn-l" onClick={() => discuss(me, r.pid, r.taskId, open, run)}>Обсудить</button>
            <button type="button" className="btn-l" onClick={remove}>Убрать из SMM</button>
          </div>)}
      </div>);
  }
  const WIN_TABS = [['active', 'Актуальные'], ['open', 'Открыто'], ['closing', 'Закрывается'], ['future', 'Впереди'], ['closed', 'Закрыто'], ['all', 'Все']];
  function WindowsModal({ me, d, sel, one, open, close }) {
    const [tab, setTab] = React.useState(WIN_TABS.some(([k]) => k === sel) ? sel : 'active');
    // #windows=<pid> — ссылки других кабинетов вида ?project=<id>: окна этого объекта
    const [pid, setPid] = React.useState(sel && d.consent.some((p) => p.pid === sel) ? sel : 'all');
    const [fmt, setFmt] = React.useState('all');
    const [book, setBook] = React.useState(one || null);
    const inTab = (k) => (r) => (k === 'all' ? true : k === 'active' ? r.window.state !== 'closed' : r.window.state === k);
    const base = one ? d.windows.filter((r) => r.pid + '~' + r.taskId === one) : d.windows;
    // сначала то, что закрывается и открыто сейчас, затем работы впереди и закрытые — внутри по концу окна (как отдаёт стор)
    const RANK = { closing: 0, open: 1, future: 2, closed: 3 };
    const list = one ? base : base.filter(inTab(tab)).filter((r) => (pid === 'all' || r.pid === pid) && (fmt === 'all' || r.formats.includes(fmt))).sort((a, b) => RANK[a.window.state] - RANK[b.window.state]);
    return (
      <Modal size="wide" title={one ? 'Работа для съёмки' : 'Окна съёмок'} sub={one ? (base[0] ? base[0].obj : '') : 'окно — со сдачи работы до старта закрывающего этапа · бронь согласует прораб'} onClose={close}>
        {!one && <React.Fragment>
          <div className="mg-tabs"><W.CalTabs kinds={WIN_TABS} tab={tab} setTab={setTab} counts={WIN_TABS.reduce((a, [k]) => Object.assign(a, { [k]: d.windows.filter(inTab(k)).length }), {})} label="Какие окна показать" /></div>
          <div className="pz-row">
            <select className="pz-in mk-sel" value={pid} onChange={(e) => setPid(e.target.value)} aria-label="Объект"><option value="all">Все объекты</option>{d.consent.map((p) => <option key={p.pid} value={p.pid}>{p.obj}</option>)}</select>
            <select className="pz-in mk-sel" value={fmt} onChange={(e) => setFmt(e.target.value)} aria-label="Формат"><option value="all">Все форматы</option>{FORMATS().map((x) => <option key={x.id} value={x.id}>{x.label}</option>)}</select>
          </div></React.Fragment>}
        {list.length ? <div className="cv-cards mk-cards">{list.map((r) => <WinCard key={r.pid + r.taskId} r={r} me={me} open={open} book={book === r.pid + '~' + r.taskId} setBook={setBook} />)}</div>
          : <div className="note-s" style={{ marginTop: 18 }}><span>{d.windows.length ? 'По этим фильтрам работ нет.' : 'SMM-работ пока нет: они берутся из каталога, от дизайнера или добавляются вручную.'}</span></div>}
        {!one && <ManualAdd me={me} />}
      </Modal>);
  }
  function ManualAdd({ me }) {
    const projects = P.views.smmProjects('marketing');
    const [pid, setPid] = React.useState('');
    const [tid, setTid] = React.useState('');
    const [run, e, ok] = useRun();
    const pr = projects.find((p) => p.pid === pid);
    const removed = projects.flatMap((p) => p.tasks.filter((t) => t.manual === false).map((t) => Object.assign({}, t, { pid: p.pid, obj: p.obj })));
    const restore = (r) => run(() => { P.setSmmManual(r.pid, r.taskId, null, actor(me)); const t = P.get(r.pid).tasks.find((x) => x.id === r.taskId); if (!P.isSmm(t)) P.setSmmManual(r.pid, r.taskId, true, actor(me)); }, 'Работа вернулась в SMM');
    return (
      <section className="dz-sec">
        <div className="t">Добавить работу вручную</div>
        <div className="lab">Если работа не помечена в каталоге и дизайнер её не выделил.</div>
        <div className="pz-row">
          <select className="pz-in mk-sel" value={pid} onChange={(x) => { setPid(x.target.value); setTid(''); }} aria-label="Объект"><option value="">Объект</option>{projects.map((p) => <option key={p.pid} value={p.pid}>{p.obj}</option>)}</select>
          <select className="pz-in mk-sel" value={tid} disabled={!pr} onChange={(x) => setTid(x.target.value)} aria-label="Работа"><option value="">Работа</option>{(pr ? pr.tasks.filter((t) => !t.smm) : []).map((t) => <option key={t.taskId} value={t.taskId}>{t.name} ({t.stage})</option>)}</select>
          <button type="button" className="btn-k" disabled={!pid || !tid} onClick={() => { if (run(() => P.setSmmManual(pid, tid, true, actor(me)), 'Работа добавлена в SMM')) setTid(''); }}>Добавить в SMM</button>
        </div>
        {err(e)}{okNote(ok)}
        {removed.length > 0 && <div className="mg-rows" style={{ marginTop: 12 }}>{removed.map((r) => (
          <div key={r.pid + r.taskId} className="r"><span className="tx"><span className="t">{r.name}</span><span className="lab">{r.obj} · {r.stage} · убрана вами</span></span><button type="button" className="btn-l" onClick={() => restore(r)}>Вернуть</button></div>))}</div>}
      </section>);
  }

  /* ───── выезды ───── */
  function probeOrient(file) {
    if (!file || !/^video\//.test(file.type || '')) return Promise.resolve(null);
    return new Promise((res) => {
      const v = document.createElement('video'); const u = URL.createObjectURL(file); let fin = false;
      const end = (o) => { if (fin) return; fin = true; URL.revokeObjectURL(u); res(o); };
      v.preload = 'metadata'; v.onloadedmetadata = () => end(v.videoWidth > v.videoHeight ? 'h' : 'v'); v.onerror = () => end('v');
      setTimeout(() => end('v'), 4000); v.src = u;
    });
  }
  function ShootCard({ s, me, open, full }) {
    const [mode, setMode] = React.useState(null);
    const [busy, setBusy] = React.useState(false);
    const [date, setDate] = React.useState(s.date);
    const [time, setTime] = React.useState(s.time || '10:00');
    const [why, setWhy] = React.useState('');
    const [run, e, ok] = useRun();
    const fileRef = React.useRef(null);
    const live = MV.LIVE.includes(s.status);
    async function onFiles(ev) {
      const files = Array.from(ev.target.files || []); ev.target.value = '';
      if (!files.length || !window.Files) return;
      setBusy(true);
      try {
        const ups = await window.Files.uploadAll(files, { pid: s.pid, kind: 'shoot' });
        const good = ups.filter((u) => !u.local);
        if (!good.length) { run(() => { throw new Error('Файлы не загрузились на сервер — проверьте соединение и повторите.'); }); return; }
        run(() => P.completeShoot(s.pid, s.id, good.map((u) => ({ name: u.name, url: u.url, kind: u.kind === 'video' ? 'video' : 'image' })), actor(me)),
          good.length < ups.length ? 'Сохранено ' + good.length + ' из ' + ups.length + ' файлов — остальные не загрузились' : 'Съёмка завершена, материал в медиатеке');
      } finally { setBusy(false); }
    }
    return (
      <div className={'card mk-card' + (s.status === 'moved' && s.movedBy === 'foreman' ? ' is-over' : '')}>
        <div className="cv-chead"><div style={{ minWidth: 0 }}>
          {full ? <div className="t">{dr(s.date)}{s.time ? ', ' + s.time : ''} · {s.tasks.join(', ')}</div>
            : <button type="button" className="mk-link t" onClick={() => open('shoot=' + s.pid + '~' + s.id)}>{dr(s.date)}{s.time ? ', ' + s.time : ''} · {s.tasks.join(', ')}</button>}
          <div className="lab" style={{ marginTop: 3 }}>{s.obj}{s.address && s.address !== s.obj ? ' · ' + s.address : ''}</div></div><ShootPill s={s} /></div>
        <dl className="cv-meta mk-meta"><div><dt>Согласие</dt><dd><Cons c={s.consent} /></dd></div>
          {s.note ? <div><dt>Задача съёмки</dt><dd>{s.note}</dd></div> : null}
          {s.moveReason && s.status === 'moved' ? <div><dt>Причина переноса</dt><dd>{s.moveReason}</dd></div> : null}
          {s.cancelReason && s.status === 'canceled' ? <div><dt>Причина отмены</dt><dd>{s.cancelReason}</dd></div> : null}</dl>
        {s.consent === 'none' && <div className="lab" style={{ marginTop: 6 }}>Согласия нет: снимать для архива можно, публиковать — нельзя.</div>}
        {s.consent === 'anonymous' && <div className="lab" style={{ marginTop: 6 }}>Без лиц и номеров домов — адрес в кадр не попадает.</div>}
        {full && live && s.prep.length > 0 && <div className="dz-note">Подготовка объекта: {s.prep.join(' · ')}</div>}
        {s.status === 'done' && s.media.length > 0 && <div className="dz-files">{s.media.slice(0, 8).map((m) => <span key={m.url} className="dz-th">{m.kind === 'video' ? <video src={m.url} muted preload="metadata" /> : <img src={m.url} alt="" loading="lazy" />}</span>)}{s.media.length > 8 && <span className="lab">+{s.media.length - 8}</span>}</div>}
        {err(e)}{okNote(ok)}
        {live && full && (
          <div className="dz-acts">
            {s.status === 'moved' && s.movedBy === 'foreman' && <button type="button" className="btn-k" onClick={() => run(() => P.confirmShoot(s.pid, s.id, actor(me)), 'Новая дата подтверждена')}>Подтвердить новую дату</button>}
            {s.status === 'confirmed' && s.date <= t0() && <React.Fragment><input ref={fileRef} type="file" accept="image/*,video/*" multiple hidden onChange={onFiles} />
              <button type="button" className="btn-k" disabled={busy} onClick={() => fileRef.current && fileRef.current.click()}>{busy ? 'Загрузка…' : 'Завершить и загрузить'}</button></React.Fragment>}
            {(s.status === 'requested' || (s.status === 'moved' && s.movedBy !== 'foreman')) && <span className="lab">Ждём подтверждения прораба</span>}
            <button type="button" className="btn-l" onClick={() => discuss(me, s.pid, s.taskIds[0], open, run)}>Обсудить</button>
            <button type="button" className="btn-l" onClick={() => setMode(mode === 'move' ? null : 'move')}>Перенести</button>
            <button type="button" className="btn-l" onClick={() => setMode(mode === 'cancel' ? null : 'cancel')}>Отменить</button>
          </div>)}
        {mode === 'move' && <div className="pz-slot">
          <label><span className="lab">Новая дата</span><input className="pz-in" type="date" value={date} min={t0()} onChange={(x) => setDate(x.target.value)} /></label>
          <label><span className="lab">Время</span><input className="pz-in" type="time" value={time} onChange={(x) => setTime(x.target.value)} /></label>
          <label style={{ gridColumn: 'span 2' }}><span className="lab">Причина</span><input className="pz-in" value={why} placeholder="Обязательно — увидит прораб" onChange={(x) => setWhy(x.target.value)} /></label>
          <div className="dz-acts"><button type="button" className="btn-k" disabled={!why.trim()} onClick={() => { if (run(() => P.moveShoot(s.pid, s.id, date, time, why.trim(), actor(me)), 'Перенос отправлен прорабу')) setMode(null); }}>Перенести</button></div></div>}
        {mode === 'cancel' && <div className="pz-slot">
          <label style={{ gridColumn: '1 / -1' }}><span className="lab">Причина отмены</span><input className="pz-in" value={why} onChange={(x) => setWhy(x.target.value)} /></label>
          <div className="dz-acts"><button type="button" className="btn-k" disabled={!why.trim()} onClick={() => { if (run(() => P.cancelShoot(s.pid, s.id, why.trim(), actor(me)), 'Выезд отменён')) setMode(null); }}>Отменить выезд</button></div></div>}
      </div>);
  }
  function ShootsModal({ me, d, open, close }) {
    const [tab, setTab] = React.useState('live');
    const kinds = [['live', 'Предстоящие'], ['past', 'Завершённые и отменённые']];
    const live = d.shoots.filter((s) => MV.LIVE.includes(s.status));
    const past = d.shoots.filter((s) => !MV.LIVE.includes(s.status)).reverse();
    const list = tab === 'live' ? live : past;
    return (
      <Modal size="wide" title="Выезды" sub="бронь уходит прорабу · после съёмки загрузите материал — он попадёт в медиатеку" onClose={close}>
        <div className="mg-tabs"><W.CalTabs kinds={kinds} tab={tab} setTab={setTab} counts={{ live: live.length, past: past.length }} label="Какие выезды показать" /></div>
        {list.length ? <div className="cv-cards mk-cards">{list.map((s) => <ShootCard key={s.pid + s.id} s={s} me={me} open={open} />)}</div>
          : <div className="note-s" style={{ marginTop: 18 }}><span>{tab === 'live' ? 'Выездов пока нет — забронируйте дату в «Окнах съёмок».' : 'Прошедших выездов нет.'}</span></div>}
      </Modal>);
  }
  function ShootModal({ me, d, arg, open, close }) {
    const [pid, id] = arg.split('~');
    const s = d.shoots.find((x) => x.pid === pid && x.id === id);
    return (
      <Modal size="wide" title="Выезд на съёмку" sub={s ? s.obj : ''} onClose={close}>
        {s ? <div className="cv-cards mk-cards"><ShootCard s={s} me={me} open={open} full /></div> : <div className="note-s" style={{ marginTop: 18 }}><span>Выезд не найден.</span></div>}
        <div className="dz-acts"><button type="button" className="cv-arrow-btn" onClick={() => open('shoots')}>Все выезды<Ic n="ne" s={14} /></button></div>
      </Modal>);
  }

  /* ───── контент-план ───── */
  const PLAN_TABS = [['ideas', 'Идеи'], ['work', 'В работе'], ['review', 'На проверке'], ['ready', 'Готовы'], ['published', 'Опубликованы']];
  const inPlan = (k) => (p) => (k === 'work' ? ['idea', 'shoot', 'edit'].includes(p.status) : p.status === k);
  function PlanModal({ me, d, sel, open, close }) {
    const C = window.ContentPlan;
    const [tab, setTab] = React.useState(PLAN_TABS.some(([k]) => k === sel) ? sel : 'ideas');
    const [all, setAll] = React.useState(false);
    const [run, e] = useRun();
    if (!C) return <Modal title="Контент-план" onClose={close}><div className="note-s" style={{ marginTop: 18 }}><span>Контент-план не загрузился.</span></div></Modal>;
    const ideas = MV.ideas(all);
    const counts = PLAN_TABS.reduce((a, [k]) => Object.assign(a, { [k]: k === 'ideas' ? ideas.total : d.posts.filter(inPlan(k)).length }), {});
    const plan = (key) => run(() => { const p = C.fromIdea(key, actor(me)); if (p) open('post=' + p.id); });
    const newPost = () => run(() => { const p = C.createPost({ title: 'Новый пост', date: t0(), channels: ['instagram'], format: 'result' }, actor(me)); if (p) open('post=' + p.id); });
    const posts = d.posts.filter(inPlan(tab));
    return (
      <Modal size="wide" title="Контент-план" sub="посты по объектам и без · проверка руководителем · публикация вручную со ссылками" onClose={close}>
        <div className="mg-tabs"><W.CalTabs kinds={PLAN_TABS} tab={tab} setTab={setTab} counts={counts} label="Что показать" /></div>
        <div className="pz-row"><button type="button" className="btn-k" onClick={newPost}>Новый пост</button></div>
        {err(e)}
        {tab === 'ideas' ? (
          <React.Fragment>
            <div className="lab" style={{ marginTop: 12 }}>{all ? 'Все идеи из плана работ' : 'Идеи на ' + MV.HORIZON + ' дней вперёд'} — работы, которые стоит снять. Без согласия клиента — только архив.</div>
            {ideas.shown.length ? <div className="mg-rows" style={{ marginTop: 6 }}>{ideas.shown.map((i) => (
              <div key={i.key} className="r"><span className="tx"><span className="t">{i.name}</span><span className="lab">{i.obj} · {dr(i.date)}{i.shoot ? ' · выезд ' + MV.SHOOT_L[i.shoot.status].toLowerCase() : ''}{i.formats.length ? ' · ' + i.formats.map(fmtL).join(', ') : ''}</span></span>
                <Cons c={i.consent} />
                <button type="button" className="btn-k" onClick={() => plan(i.key)}>В план</button>
                <button type="button" className="btn-l" onClick={() => run(() => C.dismissIdea(i.key, actor(me)))}>Скрыть</button></div>))}</div>
              : <div className="note-s" style={{ marginTop: 12 }}><span>Идей {all ? '' : 'на месяц '}нет — они появятся из плана работ.</span></div>}
            {!all && ideas.later > 0 && <div className="dz-acts"><button type="button" className="btn-l" onClick={() => setAll(true)}>Показать все · ещё {ideas.later}</button></div>}
            {all && <div className="dz-acts"><button type="button" className="btn-l" onClick={() => setAll(false)}>Только {MV.HORIZON} дней</button></div>}
          </React.Fragment>
        ) : posts.length ? <div className="mg-list">{posts.map((p) => (
          <button type="button" key={p.id} className="mg-deal" onClick={() => open('post=' + p.id)}>
            <span className="t">{p.title}</span><span className="lab">{p.date ? dr(p.date) + (p.time ? ', ' + p.time : '') : 'без даты'} · {(p.channels || []).map((c) => (C.CHANNELS.find((x) => x.id === c) || { label: c }).label).join(', ')} · медиа {(p.mediaRefs || []).length}</span>
            <span className="r"><b className={p.status === 'edit' && p.review && p.review.verdict === 'return' ? 'mg-late' : ''}>{p.status === 'edit' && p.review && p.review.verdict === 'return' ? 'Вернули' : MV.POST_L[p.status]}</b></span>
          </button>))}</div> : <div className="note-s" style={{ marginTop: 18 }}><span>Постов в этом статусе нет.</span></div>}
      </Modal>);
  }

  function Thumb({ m }) {
    return <span className="dz-th">{m.kind === 'video' ? <video src={m.url} muted preload="metadata" /> : <img src={m.url} alt={m.name || ''} loading="lazy" />}</span>;
  }
  function PostModal({ me, id, open, close }) {
    const C = window.ContentPlan;
    const post = C ? C.get(id) : null;
    const [draft, setDraft] = React.useState(() => (post ? JSON.parse(JSON.stringify(post)) : null));
    const [links, setLinks] = React.useState({});
    const [lib, setLib] = React.useState(false);
    const [run, e, ok] = useRun();
    if (!post || !draft) return <Modal title="Пост" onClose={close}><div className="note-s" style={{ marginTop: 18 }}><span>Пост не найден — возможно, его удалили.</span></div></Modal>;
    const a = actor(me);
    const locked = post.status === 'published';
    const set = (k, v) => setDraft(Object.assign({}, draft, { [k]: v }));
    const patch = () => ({ title: String(draft.title || '').trim(), format: draft.format, channels: draft.channels, date: draft.date, time: draft.time, text: draft.text, hashtags: draft.hashtags, mediaRefs: draft.mediaRefs });
    const dirty = JSON.stringify(patch()) !== JSON.stringify({ title: post.title, format: post.format, channels: post.channels, date: post.date, time: post.time, text: post.text, hashtags: post.hashtags, mediaRefs: post.mediaRefs });
    const fresh = () => { const p = C.get(id); if (p) setDraft(JSON.parse(JSON.stringify(p))); };
    const act = (fn, msg) => { if (run(fn, msg)) fresh(); };
    const save = () => { if (dirty) C.updatePost(id, patch(), a); };
    const move = (to, msg) => act(() => { save(); if (C.get(id).status !== to) C.setStatus(id, to, a); }, msg);
    const have = (url) => (draft.mediaRefs || []).some((m) => m.url === url);
    const addMedia = (list) => set('mediaRefs', (draft.mediaRefs || []).concat(list.filter((m) => !have(m.url)).map((m) => ({ pid: m.pid, url: m.url, kind: m.kind, name: m.name || '' }))));
    const fromShoot = C.shootMedia(id).filter((m) => !have(m.url));
    const library = lib ? MV.media().filter((m) => !have(m.url) && !m.archiveOnly).slice(0, 40) : [];
    const chL = (c) => (C.CHANNELS.find((x) => x.id === c) || { label: c }).label;
    const returned = post.status === 'edit' && post.review && post.review.verdict === 'return';
    const copyText = () => { const t = [draft.text, draft.hashtags].filter(Boolean).join('\n\n'); try { navigator.clipboard.writeText(t); act(() => {}, 'Текст и хэштеги скопированы'); } catch (x) { act(() => { throw new Error('Не удалось скопировать — выделите текст вручную'); }); } };
    return (
      <Modal size="wide" title={post.title || 'Пост'} sub={MV.POST_L[post.status] + (post.date ? ' · ' + dr(post.date) + (post.time ? ', ' + post.time : '') : '')} onClose={() => { if (!dirty || window.confirm('Закрыть без сохранения правок?')) close(); }}>
        {returned && <div className="att" style={{ marginTop: 12 }}><Ic n="alert" s={16} /><span>Руководитель вернул: {post.review.comment}</span></div>}
        {post.status === 'review' && <div className="lab" style={{ marginTop: 12 }}>Ждёт решения руководителя. Правка вернёт пост в «монтаж».</div>}
        {post.status === 'ready' && <div className="lab" style={{ marginTop: 12 }}>Одобрен руководителем. Правка вернёт пост в «монтаж» — понадобится новая проверка.</div>}
        <div className="pz-slot mk-form">
          <label style={{ gridColumn: '1 / -1' }}><span className="lab">Название</span><input className="pz-in" value={draft.title} disabled={locked} onChange={(x) => set('title', x.target.value)} /></label>
          <label><span className="lab">Дата</span><input className="pz-in" type="date" value={draft.date || ''} disabled={locked} onChange={(x) => set('date', x.target.value)} /></label>
          <label><span className="lab">Время</span><input className="pz-in" type="time" value={draft.time || ''} disabled={locked} onChange={(x) => set('time', x.target.value)} /></label>
          <label><span className="lab">Формат</span><select className="pz-in" value={draft.format} disabled={locked} onChange={(x) => set('format', x.target.value)}>{C.POST_FORMATS.map((f) => <option key={f.id} value={f.id}>{f.label}</option>)}</select></label>
          <div style={{ gridColumn: '1 / -1' }}><span className="lab">Каналы</span><div className="mk-chips">{C.CHANNELS.map((c) => <button key={c.id} type="button" disabled={locked} aria-pressed={draft.channels.includes(c.id)} className={'mk-chip' + (draft.channels.includes(c.id) ? ' on' : '')} onClick={() => set('channels', draft.channels.includes(c.id) ? draft.channels.filter((x) => x !== c.id) : draft.channels.concat([c.id]))}>{c.label}</button>)}</div></div>
          <label style={{ gridColumn: '1 / -1' }}><span className="lab">Текст</span><textarea className="cv-ta" rows={5} value={draft.text || ''} disabled={locked} onChange={(x) => set('text', x.target.value)} /></label>
          <label style={{ gridColumn: '1 / -1' }}><span className="lab">Хэштеги</span><input className="pz-in" value={draft.hashtags || ''} placeholder="#ремонт #санузел" disabled={locked} onChange={(x) => set('hashtags', x.target.value)} /></label>
        </div>
        <section className="dz-sec"><div className="t">Медиа · {(draft.mediaRefs || []).length}</div>
          <div className="dz-files">{(draft.mediaRefs || []).map((m) => (
            <span key={m.url} className="mk-thw"><Thumb m={m} />{!locked && <button type="button" className="mk-x" aria-label="Убрать" onClick={() => set('mediaRefs', draft.mediaRefs.filter((x) => x.url !== m.url))}><Ic n="x" s={12} /></button>}</span>))}</div>
          {!locked && <div className="dz-acts">
            {fromShoot.length > 0 && <button type="button" className="btn-k" onClick={() => addMedia(fromShoot)}>Фото со съёмки: {fromShoot.length} — добавить</button>}
            <button type="button" className="btn-l" onClick={() => setLib(!lib)}>{lib ? 'Скрыть медиатеку' : 'Из медиатеки'}</button></div>}
          {lib && !locked && (library.length ? <div className="dz-files">{library.map((m) => <button key={m.url} type="button" className="mk-pick" onClick={() => addMedia([m])} title={(m.taskName || m.name || '') + ' · ' + m.obj}><Thumb m={m} /></button>)}</div>
            : <div className="lab" style={{ marginTop: 8 }}>В медиатеке нет файлов, которые можно публиковать.</div>)}
        </section>
        {post.status === 'ready' && <section className="dz-sec"><div className="t">Ссылки на публикации</div>
          <div className="pz-slot">{post.channels.map((c) => <label key={c}><span className="lab">{chL(c)}</span><input className="pz-in" value={links[c] || ''} placeholder="https://…" onChange={(x) => setLinks(Object.assign({}, links, { [c]: x.target.value }))} /></label>)}</div></section>}
        {post.status === 'published' && <section className="dz-sec"><div className="t">Опубликовано</div>
          <div className="mg-rows">{Object.keys(post.publishedLinks || {}).map((c) => <div key={c} className="r"><span className="tx"><span className="t">{chL(c)}</span><a className="lab" href={post.publishedLinks[c]} target="_blank" rel="noopener noreferrer">{post.publishedLinks[c]}</a></span></div>)}</div></section>}
        {err(e)}{okNote(ok)}
        <div className="dz-acts mk-foot">
          {!locked && dirty && <button type="button" className="btn-k" onClick={() => act(() => C.updatePost(id, patch(), a), 'Сохранено')}>Сохранить</button>}
          {!locked && dirty && <button type="button" className="btn-l" onClick={fresh}>Отменить правки</button>}
          {post.status === 'idea' && <button type="button" className="btn-l" onClick={() => move('shoot', 'Пост — в съёмке')}>В съёмку</button>}
          {(post.status === 'idea' || post.status === 'shoot') && <button type="button" className="btn-l" onClick={() => move('edit', 'Пост — в монтаже')}>В монтаж</button>}
          {post.status === 'edit' && <button type="button" className="btn-l" onClick={() => move('shoot', 'Пост — в съёмке')}>Переснять</button>}
          {post.status === 'edit' && <button type="button" className="btn-k" onClick={() => act(() => { save(); C.submitReview(id, a); }, 'Отправлено руководителю')}>На проверку руководителю</button>}
          {post.status === 'ready' && <button type="button" className="btn-k" onClick={() => act(() => { if (dirty) throw new Error('Есть несохранённые правки: сохраните их (пост вернётся на проверку) или отмените'); C.markPublished(id, links, a); }, 'Пост опубликован')}>Отметить опубликованным</button>}
          {post.status === 'ready' && <button type="button" className="btn-l" onClick={() => move('edit', 'Пост снят с публикации')}>Снять с публикации</button>}
          {(post.status === 'ready' || locked) && <button type="button" className="btn-l" onClick={copyText}>Скопировать текст</button>}
          {!locked && <button type="button" className="btn-l" onClick={() => { if (window.confirm('Удалить пост «' + post.title + '»?') && run(() => C.removePost(id, a))) open('plan'); }}>Удалить</button>}
        </div>
      </Modal>);
  }

  /* ───── медиатека: скачать, выбрать несколько «в пост», «Показать клиенту», «Для сделок»; файлы без превью скрываются ───── */
  const PHASE_L = { '00': '/00 Дизайн', '01': '/01 Подготовка', '02': '/02 Черновые', '03': '/03 Предчистовая', '04': '/04 Чистовая', '05': '/05 Сдача' };
  function DealUse({ m, me, run }) {
    const dd = m.deal || { on: false, steps: [], phase: null, objection: '' };
    const set = (p) => run(() => P.setSmmDealUse(m.pid, m.mediaId, p, actor(me)));
    return (
      <div className="mk-deal">
        <label className="mk-tg" title="Материал попадёт в кабинет клиента до договора: на главную, в окно «Замер» или в смету-КП"><input type="checkbox" checked={!!dd.on} onChange={(x) => set({ on: x.target.checked })} />Для сделок</label>
        {dd.on && <React.Fragment>
          <div className="mk-chips" role="group" aria-label="Шаги клиента">{(P.DEAL_STEPS || []).map((st) => <button key={st} type="button" aria-pressed={dd.steps.includes(st)} className={'mk-chip' + (dd.steps.includes(st) ? ' on' : '')} onClick={() => set({ steps: dd.steps.includes(st) ? dd.steps.filter((x) => x !== st) : dd.steps.concat([st]) })}>{P.DEAL_STEP_L[st]}</button>)}</div>
          {!dd.steps.length && <div className="lab mk-warn">Отметьте хотя бы один шаг, иначе клиент материал не увидит</div>}
          <select className="pz-in mk-sel" value={dd.phase || ''} onChange={(x) => set({ phase: x.target.value || null })} aria-label="Фаза сметы"><option value="">Любая фаза</option>{Object.keys(PHASE_L).map((k) => <option key={k} value={k}>{PHASE_L[k]}</option>)}</select>
          <select className="pz-in mk-sel" value={dd.objection || ''} onChange={(x) => set({ objection: x.target.value })} aria-label="Сомнение клиента"><option value="">Любое сомнение</option>{(P.DEAL_OBJECTIONS || []).map((k) => <option key={k} value={k}>{cap(P.DEAL_OBJECTION_L[k])}</option>)}</select>
        </React.Fragment>}
      </div>);
  }
  const keyOf = (m) => m.pid + '|' + m.url;
  function MediaModal({ me, d, open, close }) {
    const C = window.ContentPlan;
    const [pid, setPid] = React.useState('all');
    const [fmt, setFmt] = React.useState('all');
    const [sel, setSel] = React.useState([]);
    const [bad, setBad] = React.useState([]);
    const [big, setBig] = React.useState(null);
    const [to, setTo] = React.useState('');
    const [upPid, setUpPid] = React.useState('');
    const [upTask, setUpTask] = React.useState('');
    const [busy, setBusy] = React.useState(false);
    const [run, e, ok] = useRun();
    const fileRef = React.useRef(null);
    const rows = MV.media({ pid: pid === 'all' ? undefined : pid, format: fmt === 'all' ? undefined : fmt }).filter((m) => !bad.includes(m.url));
    const picked = rows.filter((m) => sel.includes(keyOf(m)));
    const pub = picked.filter((m) => !m.archiveOnly);
    const open4post = C ? C.list().filter((p) => p.status !== 'published') : [];
    const toggle = (m) => setSel(sel.includes(keyOf(m)) ? sel.filter((k) => k !== keyOf(m)) : sel.concat([keyOf(m)]));
    const refs = (list) => list.map((m) => ({ pid: m.pid, url: m.url, kind: m.kind, name: m.name || '' }));
    const toPost = () => run(() => {
      if (!pub.length) throw new Error('Выбранные файлы — только архив: без согласия клиента публиковать нельзя');
      let p;
      if (to) { const cur = C.get(to); const have = (cur.mediaRefs || []).map((x) => x.url); p = C.updatePost(to, { mediaRefs: (cur.mediaRefs || []).concat(refs(pub).filter((x) => !have.includes(x.url))) }, actor(me)); }
      else p = C.createPost({ title: (pub[0].taskName || 'Новый пост'), date: t0(), channels: ['instagram'], format: 'result', mediaRefs: refs(pub) }, actor(me));
      setSel([]); if (p) open('post=' + p.id);
    });
    const download = () => picked.forEach((m, i) => setTimeout(() => { const a = document.createElement('a'); a.href = m.url; a.download = m.name || ''; a.target = '_blank'; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); }, i * 250));
    async function onFiles(ev) {
      const files = Array.from(ev.target.files || []); ev.target.value = '';
      if (!files.length || !upPid || !window.Files) return;
      setBusy(true);
      try {
        const ups = await window.Files.uploadAll(files, { pid: upPid, kind: 'smm' });
        const orients = await Promise.all(files.map(probeOrient));
        const good = ups.map((u, i) => Object.assign({}, u, { orient: orients[i] })).filter((u) => !u.local);
        run(() => {
          if (!good.length) throw new Error('Файлы не загрузились на сервер — проверьте соединение и повторите.');
          good.forEach((u) => P.addSmmMedia(upPid, { url: u.url, name: u.name, kind: u.kind === 'video' ? 'video' : 'image', orient: u.orient, taskId: upTask || null, from: 'upload' }, actor(me)));
        }, 'Загружено: ' + good.length);
      } finally { setBusy(false); }
    }
    const upProj = d.consent.find((p) => p.pid === upPid);
    const upTasks = upProj ? (P.views.smmProjects('marketing').find((p) => p.pid === upPid) || { tasks: [] }).tasks.filter((t) => t.smm) : [];
    return (
      <Modal size="wide" title="Медиатека" sub="съёмки, фото сдачи SMM-работ и медиа из чатов · без согласия клиента — только архив" onClose={close}>
        <div className="pz-row">
          <select className="pz-in mk-sel" value={pid} onChange={(x) => setPid(x.target.value)} aria-label="Объект"><option value="all">Все объекты</option>{d.consent.map((p) => <option key={p.pid} value={p.pid}>{p.obj}</option>)}</select>
          <select className="pz-in mk-sel" value={fmt} onChange={(x) => setFmt(x.target.value)} aria-label="Формат"><option value="all">Все форматы</option>{FORMATS().map((x) => <option key={x.id} value={x.id}>{x.label}</option>)}</select>
          <span className="lab">{rows.length} {plural(rows.length, 'файл', 'файла', 'файлов')}{bad.length ? ' · ' + bad.length + ' без превью скрыто' : ''}</span>
        </div>
        {err(e)}{okNote(ok)}
        {rows.length ? <div className="mk-grid">{rows.map((m) => {
          const on = sel.includes(keyOf(m));
          return (
            <div key={keyOf(m)} className={'mk-m' + (on ? ' on' : '')}>
              <button type="button" className="mk-mph" onClick={() => setBig(m)} aria-label={'Открыть ' + (m.taskName || m.name || 'материал')}>
                {m.kind === 'video' ? <video src={m.url} muted preload="metadata" onError={() => setBad(bad.concat([m.url]))} /> : <img src={m.url} alt={m.name || ''} loading="lazy" onError={() => setBad(bad.concat([m.url]))} />}
                {m.kind === 'video' && <span className="mk-play" aria-hidden="true">▶</span>}
                {m.deal && m.deal.on && m.deal.steps.length > 0 && <span className="mk-flag">Для сделок</span>}
              </button>
              <label className="mk-ck" title="Выбрать"><input type="checkbox" checked={on} onChange={() => toggle(m)} aria-label={'Выбрать ' + (m.taskName || m.name || 'файл')} /></label>
              <div className="mk-mtx">
                <div className="t" title={m.taskName || m.name}>{m.taskName || m.name || 'Без названия'}</div>
                <div className="lab">{short(m.obj)} · {MEDIA_SRC[m.source]}{m.at ? ', ' + dr(m.at) : ''}</div>
                <div className="mk-mrow">{m.archiveOnly ? <span className="pill-d">Только архив</span> : <Cons c={m.consent} />}
                  <a className="cv-arrow-btn" href={m.url} download={m.name || ''} target="_blank" rel="noopener noreferrer">Скачать</a></div>
                {m.mediaId && m.kind === 'video' && <label className="mk-tg" title="Видео появится в ленте «Новое на объекте» у клиента; привязанное к работе — после её приёмки"><input type="checkbox" checked={!!m.clientVisible} onChange={(x) => run(() => P.setSmmClientVisible(m.pid, m.mediaId, x.target.checked, actor(me)))} />Показать клиенту</label>}
                {m.mediaId && !m.archiveOnly && <DealUse m={m} me={me} run={run} />}
              </div>
            </div>);
        })}</div> : <div className="note-s" style={{ marginTop: 18 }}><span>Материалов пока нет — они появятся после съёмок, сдачи работ или загрузки.</span></div>}
        {picked.length > 0 && <div className="pz-bat">
          <span className="t">Выбрано {picked.length}{picked.length > pub.length ? ' · ' + (picked.length - pub.length) + ' только архив — в пост не попадут' : ''}</span>
          <button type="button" className="btn-l" onClick={download}>Скачать выбранные</button>
          {C && <select className="pz-in mk-sel" value={to} onChange={(x) => setTo(x.target.value)} aria-label="Куда добавить"><option value="">В новый пост</option>{open4post.map((p) => <option key={p.id} value={p.id}>В пост «{p.title}»</option>)}</select>}
          {C && <button type="button" className="btn-k" disabled={!pub.length} onClick={toPost}>В пост · {pub.length}</button>}
          <button type="button" className="btn-l" onClick={() => setSel([])}>Снять выбор</button>
        </div>}
        <section className="dz-sec"><div className="t">Загрузить материал</div>
          <div className="pz-row">
            <select className="pz-in mk-sel" value={upPid} onChange={(x) => { setUpPid(x.target.value); setUpTask(''); }} aria-label="Объект загрузки"><option value="">Объект</option>{d.consent.map((p) => <option key={p.pid} value={p.pid}>{p.obj}</option>)}</select>
            <select className="pz-in mk-sel" value={upTask} disabled={!upProj} onChange={(x) => setUpTask(x.target.value)} aria-label="Работа"><option value="">Без привязки к работе</option>{upTasks.map((t) => <option key={t.taskId} value={t.taskId}>{t.name}</option>)}</select>
            <input ref={fileRef} type="file" accept="image/*,video/*" multiple hidden onChange={onFiles} />
            <button type="button" className="btn-k" disabled={!upPid || busy} onClick={() => fileRef.current && fileRef.current.click()}>{busy ? 'Загрузка…' : 'Выбрать файлы'}</button>
          </div></section>
        {big && <div className="mk-lb" role="dialog" aria-label="Просмотр" onClick={() => setBig(null)}>
          {big.kind === 'video' ? <video src={big.url} controls autoPlay onClick={(x) => x.stopPropagation()} /> : <img src={big.url} alt={big.name || ''} />}</div>}
      </Modal>);
  }

  /* ───── согласие клиентов ───── */
  function ConsentModal({ me, d, close }) {
    const [run, e, ok] = useRun();
    const rows = d.consent.slice().sort((a, b) => (b.open - a.open) || a.obj.localeCompare(b.obj));
    return (
      <Modal size="wide" title="Согласие клиентов" sub="что можно публиковать · уровень выбирает клиент в своём кабинете" onClose={close}>
        <div className="pz-meta">{['public', 'anonymous', 'none'].map((l) => <div key={l}><dt>{MV.CONSENT_L[l]}</dt><dd>{d.f.consent[l] || 0}</dd></div>)}<div><dt>Запрошено, ждём ответа</dt><dd>{d.f.asked}</dd></div></div>
        {err(e)}{okNote(ok)}
        {rows.length ? <div className="mg-rows" style={{ marginTop: 12 }}>{rows.map((p) => (
          <div key={p.pid} className="r"><span className="tx"><span className="t">{p.obj}</span>
            <span className="lab">{p.smm} SMM-{plural(p.smm, 'работа', 'работы', 'работ')}{p.open ? ' · ' + p.open + ' ещё можно снять' : ''}{p.asked ? ' · запрошено ' + dr(p.askedAt) : p.answeredAt ? ' · ответил ' + dr(p.answeredAt) : ''}</span></span>
            <Cons c={p.consent} />
            {p.consent !== 'public' && (p.canAsk ? <button type="button" className="btn-k" onClick={() => run(() => P.requestMarketingConsent(p.pid, actor(me)), 'Запрос отправлен клиенту: «' + p.obj + '»')}>Запросить согласие</button>
              : <span className="lab">ждём ответа</span>)}
          </div>))}</div> : <div className="note-s" style={{ marginTop: 18 }}><span>Проектов пока нет.</span></div>}
        <div className="lab" style={{ marginTop: 12 }}>Проекты без согласия видны как архив: окна и выезды планировать можно, публиковать — нельзя. Повторный запрос — не раньше чем через 7 дней.</div>
      </Modal>);
  }

  /* ───── чаты ───── */
  function Media({ m }) {
    const kind = m.kind || (/\.(mp4|mov|webm)(\?|$)/i.test(m.url) ? 'video' : /\.(png|jpe?g|gif|webp|avif)(\?|$)/i.test(m.url) || /^data:image/.test(m.url) ? 'image' : 'file');
    if (kind === 'video') return <video src={m.url} controls preload="metadata" playsInline style={{ maxWidth: '100%', borderRadius: 12 }} />;
    if (kind === 'image' || kind === 'photo') return <img src={m.url} alt={m.name || ''} />;
    return <a className="pill-n" href={m.url} target="_blank" rel="noreferrer">{m.name || 'Вложение'}</a>;
  }
  function ChatModal({ d, sel, open, close, me }) {
    const [src0, pid0, tid0] = (sel || '').split(':');
    const cur = d.chats.find((c) => (!src0 || c.src === src0) && c.pid === pid0 && (!tid0 || c.id === tid0)) || d.chats[0] || null;
    const [txt, setTxt] = React.useState('');
    const DC = window.DealChat;
    const thread = !cur ? null : cur.src === 'deal' ? (DC ? DC.load(cur.pid).threads.find((t) => t.id === cur.id) : null) : ((P.get(cur.pid) || {}).chat || { threads: [] }).threads.find((t) => t.id === cur.id);
    React.useEffect(() => { if (cur && cur.src === 'deal' && DC && DC.markRead && cur.isNew) DC.markRead(cur.pid, cur.id, 'marketing'); }, [cur && cur.pid, cur && cur.id]);
    const send = () => {
      if (!txt.trim() || !thread) return;
      if (cur.src === 'deal') DC.send(cur.pid, thread.id, { from: 'marketing', name: me.name, t: txt.trim() }); else P.chatSend(cur.pid, thread.id, { from: 'marketing', t: txt.trim() });
      setTxt('');
    };
    return (
      <Modal size="wide" title="Чаты" sub="треды по SMM-работам и сделкам: маркетолог, прораб, исполнитель, менеджер" onClose={close}>
        <div className="lw-chat">
          <div className="lw-chl">{d.chats.map((c) => (
            <button type="button" key={c.src + c.pid + c.id} className={'lw-chr' + (cur && c.src === cur.src && c.pid === cur.pid && c.id === cur.id ? ' on' : '') + (c.isNew ? ' is-new' : '')} onClick={() => open('chat=' + c.src + ':' + c.pid + ':' + c.id)}>
              <span className="t">{short(c.obj)}<span className="lab"> · {c.title}</span></span><span className="lab">{c.who ? c.who + ': ' : ''}{c.text}</span><span className="tm lab">{when(c.at)}</span></button>))}</div>
          <div className="lw-chv">{thread ? <React.Fragment>
            <div className="lw-chh"><b>{cur.obj}</b><span className="lab">{thread.members.filter((m) => m !== 'marketing').map((m) => MV.RU[m] || m).join(', ')}</span></div>
            <div className="lw-msgs cv-scroll">{thread.msgs.map((m) => (
              <div key={m.id} className={'lw-msg' + (m.from === 'marketing' ? ' mine' : '')}><span className="lab">{MV.RU[m.from] || m.from} · {when(m.at)}</span>
                {m.t ? <span className="b">{m.t}</span> : null}{m.media && m.media.url ? <Media m={m.media} /> : 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>);
  }

  /* ───── данные и маршрутизация окон ───── */
  function collect() {
    const windows = MV.windows();
    const d = { windows, shoots: MV.shoots(), posts: MV.posts(), consent: MV.consent(windows), media: MV.media(), chats: MV.chats() };
    d.f = MV.facts(d);
    return d;
  }
  function App() {
    useStore();
    const me = useMe();
    const [modal, open, close] = useHash();
    // выезд завершён → пост из «съёмки» в «монтаж» (как прежний контент-план): при открытии и после каждого изменения проектов
    React.useEffect(() => {
      const rec = () => { try { if (window.ContentPlan) window.ContentPlan.reconcile(); } catch (e) { /* план пуст */ } };
      rec();
      return P && P.subscribe ? P.subscribe(rec) : undefined;
    }, []);
    if (!MV) return <div className="cv-page"><div className="empty">Кабинет не загрузился. Обновите страницу.</div></div>;
    if (me.deny) return <W.Denied why={me.deny} />;
    const d = collect();
    const arg = (k) => (modal.indexOf(k + '=') === 0 ? modal.slice(k.length + 1) : null);
    const has = (k) => modal === k || arg(k) != null;
    const zone = has('windows') || has('win') ? 'wins' : has('shoots') || has('shoot') ? 'shoots' : has('plan') || has('post') ? 'plan' : has('media') ? 'media' : has('chat') ? 'chat' : has('consent') ? 'home' : '';
    const NAMES = { wins: 'Раздел «Окна съёмок»', shoots: 'Раздел «Выезды»', plan: 'Раздел «Контент-план»', media: 'Раздел «Медиатека»', chat: 'Раздел «Чаты»', home: 'Согласие клиентов' };
    const ok = zone ? me[zone] : true;
    return (
      <React.Fragment>
        {me.home ? <Home me={me} d={d} open={open} /> : <W.Denied title="Главная закрыта" why="Главная кабинета не входит в выданный доступ — открытые вам разделы работают по своим ссылкам." />}
        {zone && !ok && <W.Closed what={NAMES[zone]} onClose={close} />}
        {ok && has('windows') && <WindowsModal key={modal} me={me} d={d} sel={arg('windows')} open={open} close={close} />}
        {ok && arg('win') && <WindowsModal key={modal} me={me} d={d} one={arg('win')} open={open} close={close} />}
        {ok && modal === 'shoots' && <ShootsModal me={me} d={d} open={open} close={close} />}
        {ok && arg('shoot') && <ShootModal key={arg('shoot')} me={me} d={d} arg={arg('shoot')} open={open} close={close} />}
        {ok && has('plan') && <PlanModal key={modal} me={me} d={d} sel={arg('plan')} open={open} close={close} />}
        {ok && arg('post') && <PostModal key={arg('post')} me={me} id={arg('post')} open={open} close={close} />}
        {ok && modal === 'media' && <MediaModal me={me} d={d} open={open} close={close} />}
        {ok && modal === 'consent' && <ConsentModal me={me} d={d} close={close} />}
        {ok && has('chat') && <ChatModal d={d} sel={arg('chat')} open={open} close={close} me={me} />}
      </React.Fragment>);
  }

  window.MarketingV2 = { App, collect, myInbox };
  if (!window.__MARKETING_V2_NO_MOUNT) ReactDOM.createRoot(document.getElementById('root')).render(<App />);
})();
