// Main app — wires everything: desktop + mobile + chat + launch flow
const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React;

const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{
  "theme": "light",
  "accent": "blue",
  "role": "manager",
  "surface": "desktop"
}/*EDITMODE-END*/;

// Палитра «индустриально-пластиковая»: электрический синий-base + hazard-orange CTA.
const ACCENT_MAP = {
  blue:   { '--accent':'#2563eb', '--accent-hover':'#1d4ed8', '--accent-soft':'#bfdbfe' },
  amber:  { '--accent':'#ea580c', '--accent-hover':'#c2410c', '--accent-soft':'#fed7aa' },
  lime:   { '--accent':'#10b981', '--accent-hover':'#059669', '--accent-soft':'#a7f3d0' },
  violet: { '--accent':'#7c3aed', '--accent-hover':'#6d28d9', '--accent-soft':'#ede9fe' },
};
const ACCENT_MAP_DARK = {
  blue:   { '--accent':'#60a5fa', '--accent-hover':'#93c5fd', '--accent-soft':'#1e3a8a' },
  amber:  { '--accent':'#fb923c', '--accent-hover':'#fdba74', '--accent-soft':'#9a3412' },
  lime:   { '--accent':'#34d399', '--accent-hover':'#6ee7b7', '--accent-soft':'#065f46' },
  violet: { '--accent':'#a78bfa', '--accent-hover':'#c4b5fd', '--accent-soft':'#4c1d95' },
};

function useTweaks() {
  const [tweaks, setTweaks] = useStateA(() => {
    try {
      const saved = localStorage.getItem('autosvet_tweaks');
      if (saved) return { ...TWEAK_DEFAULS, ...JSON.parse(saved) };
    } catch {}
    return TWEAK_DEFAULS;
  });
  useEffectA(() => {
    localStorage.setItem('autosvet_tweaks', JSON.stringify(tweaks));
    document.documentElement.setAttribute('data-theme', tweaks.theme);
    const accentVars = tweaks.theme === 'dark' ? ACCENT_MAP_DARK[tweaks.accent] : ACCENT_MAP[tweaks.accent];
    for (const [k, v] of Object.entries(accentVars || {})) {
      document.documentElement.style.setProperty(k, v);
    }
  }, [tweaks]);
  const update = (patch) => {
    setTweaks(t => ({ ...t, ...patch }));
    try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: patch }, '*'); } catch {}
  };
  return [tweaks, update];
}

// Profile-меню в шапке. Отображает текущего залогиненного юзера + кнопку
// «Выйти». Имя/роль приходят из window.AUTH.user (после /auth/login или
// /auth/me на старте).
function ProfileMenu({ currentUser, onLogout }) {
  const [open, setOpen] = useStateA(false);
  const [showLog, setShowLog] = useStateA(false);
  const ref = useRefA(null);
  const roles = Array.isArray(window.ROLES_HIERARCHY) ? window.ROLES_HIERARCHY : [];

  // Уведомление о новой версии (PWA). HTML-регистрация SW шлёт событие
  // 'app:update-available', когда новый Service Worker установился и ЖДЁТ.
  // Тогда: зелёная точка-бейдж на иконке аккаунта + кнопка «Обновить» в
  // выпадашке версии. Клик → window.__applyUpdate() (skipWaiting + reload,
  // данные на сервере не теряются). (2026-06-18, по запросу Александра)
  const [updateReady, setUpdateReady] = useStateA(!!window.__swUpdateReady);
  const [newVer, setNewVer] = useStateA(window.__swNewVersion || null);
  useEffectA(() => {
    const h = (e) => { setUpdateReady(true); if (e && e.detail && e.detail.version) setNewVer(e.detail.version); };
    window.addEventListener('app:update-available', h);
    if (window.__swUpdateReady) setUpdateReady(true);
    return () => window.removeEventListener('app:update-available', h);
  }, []);
  const applyUpdate = () => { if (typeof window.__applyUpdate === 'function') window.__applyUpdate(); };

  useEffectA(() => {
    if (!open) return;
    const onClick = (e) => {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, [open]);

  if (!currentUser) return null;
  const myRole = roles.find(r => r.id === currentUser.role);

  return (
    <div ref={ref} style={{position:'relative'}}>
      <button
        onClick={()=>setOpen(o => !o)}
        title={currentUser.name}
        style={{
          width:32, height:32, borderRadius:'50%',
          background: myRole ? `color-mix(in oklch, ${myRole.color} 22%, transparent)` : 'var(--accent-soft)',
          color: myRole ? myRole.color : 'var(--accent)',
          border: open ? '2px solid var(--accent)' : '2px solid transparent',
          display:'flex', alignItems:'center', justifyContent:'center',
          fontSize:11, fontWeight:700, cursor:'pointer', padding:0,
        }}
      >
        {currentUser.initials}
      </button>
      {updateReady && !open && (
        <span title="Доступно обновление — откройте меню" style={{
          position:'absolute', top:-1, right:-1, width:12, height:12, borderRadius:'50%',
          background:'var(--green)', border:'2px solid var(--bg-panel)', zIndex:3,
          boxShadow:'0 0 0 1px var(--green)',
        }}/>
      )}
      {open && (
        <div style={{
          position:'absolute', top:42, right:0, minWidth:240, zIndex:200,
          background:'var(--bg-panel)', border:'1px solid var(--border)',
          borderRadius:12, boxShadow:'0 20px 50px -12px rgba(0,0,0,.22), 0 0 0 1px var(--border)',
          overflow:'visible',
        }}>
          <div style={{padding:'12px 14px', display:'flex', alignItems:'center', gap:10, borderBottom:'1px solid var(--border)'}}>
            <div style={{
              width:36, height:36, borderRadius:'50%',
              background: myRole ? `color-mix(in oklch, ${myRole.color} 22%, transparent)` : 'var(--accent-soft)',
              color: myRole ? myRole.color : 'var(--accent)',
              display:'flex', alignItems:'center', justifyContent:'center',
              fontSize:13, fontWeight:700,
            }}>{currentUser.initials}</div>
            <div style={{minWidth:0}}>
              <div style={{fontSize:13, fontWeight:600, whiteSpace:'nowrap'}}>{currentUser.name}</div>
              <div style={{fontSize:11, color: myRole?.color || 'var(--fg-muted)'}}>{myRole?.name || currentUser.role}</div>
              {currentUser.email && (
                <div style={{fontSize:10, color:'var(--fg-muted)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth:200}}>{currentUser.email}</div>
              )}
            </div>
          </div>
          {window.PWAInstallMenuItem && (
            <PWAInstallMenuItem onAfterInstall={() => setOpen(false)}/>
          )}
          <button
            onClick={()=>{ setOpen(false); if (onLogout) onLogout(); }}
            style={{
              width:'100%', display:'flex', alignItems:'center', gap:10, padding:'10px 14px',
              background:'transparent', border:'none', cursor:'pointer', color:'var(--red)',
              textAlign:'left', fontSize:13, fontWeight:500,
              borderTop:'1px solid var(--border)',
            }}
            onMouseEnter={ev => ev.currentTarget.style.background = 'var(--bg-sunk)'}
            onMouseLeave={ev => ev.currentTarget.style.background = 'transparent'}
          >
            <Icon name="arrow_left" size={14}/>
            Выйти
          </button>
          <div style={{position:'relative', padding:'6px 14px 8px', fontSize:10, color:'var(--fg-muted)', borderTop:'1px solid var(--border)', textAlign:'right', cursor:'help'}}
               onMouseEnter={() => setShowLog(true)} onMouseLeave={() => setShowLog(false)}>
            {updateReady && <span style={{display:'inline-block', width:7, height:7, borderRadius:'50%', background:'var(--green)', marginRight:6, verticalAlign:'middle'}}/>}
            v{window.APP_VERSION || '—'}
            {showLog && (updateReady || (window.APP_CHANGELOG && window.APP_CHANGELOG[window.APP_VERSION])) && (
              <div style={{position:'absolute', right:0, top:'calc(100% + 6px)', width:280,
                           background:'var(--bg-panel)', border:'1px solid var(--border)', borderRadius:8,
                           boxShadow:'0 10px 32px rgba(0,0,0,.22)', padding:'10px 12px', textAlign:'left', zIndex:300}}>
                {updateReady && (
                  <div style={{marginBottom:10, paddingBottom:10, borderBottom:'1px solid var(--border)'}}>
                    <div style={{fontSize:12, fontWeight:700, color:'var(--green)', marginBottom:7}}>
                      🆕 Доступно обновление{newVer ? ' · v' + newVer : ''}
                    </div>
                    <button onClick={applyUpdate} style={{
                      width:'100%', padding:'8px 10px', background:'var(--accent)', color:'#fff', border:'none',
                      borderRadius:7, fontSize:12, fontWeight:700, cursor:'pointer',
                    }}
                      onMouseEnter={ev => ev.currentTarget.style.filter = 'brightness(1.08)'}
                      onMouseLeave={ev => ev.currentTarget.style.filter = 'none'}>
                      Обновить сейчас
                    </button>
                    <div style={{fontSize:10, color:'var(--fg-muted)', marginTop:6, lineHeight:1.35}}>
                      Загрузит свежую версию. Данные на сервере сохранятся.
                    </div>
                  </div>
                )}
                {window.APP_CHANGELOG && window.APP_CHANGELOG[window.APP_VERSION] && (
                  <div>
                    <div style={{fontSize:11, fontWeight:700, color:'var(--fg)', marginBottom:5}}>Что нового · v{window.APP_VERSION}</div>
                    {window.APP_CHANGELOG[window.APP_VERSION].map((c, i) => (
                      <div key={i} style={{fontSize:11, color:'var(--fg-muted)', lineHeight:1.4, marginBottom:3, display:'flex', gap:5}}>
                        <span style={{color:'var(--accent)', flex:'0 0 auto'}}>•</span><span>{c}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// Полноэкранная login-модалка. Показывается когда AUTH.user === null.
// Никаких других экранов в это время не рендерим — login обязателен.
function LoginGate({ onLogin }) {
  // Обычная форма входа: логин (email) + пароль + «Войти».
  // Пароль вводится руками и НЕ хранится в коде. Единственный вход в систему
  // (browser basic-auth Caddy снят — см. deploy/Caddyfile).
  const [email, setEmail] = useStateA(() => {
    try { return localStorage.getItem('autolight.lastEmail') || ''; } catch (_) { return ''; }
  });
  const [pwd, setPwd] = useStateA('');
  const [busy, setBusy] = useStateA(false);
  const [err, setErr] = useStateA('');

  const submit = async (e) => {
    if (e && e.preventDefault) e.preventDefault();
    if (busy) return;
    const em = (email || '').trim();
    if (!em || !pwd) { setErr('Введите логин и пароль'); return; }
    setBusy(true); setErr('');
    try {
      const user = await window.AUTH.login(em, pwd);
      try { localStorage.setItem('autolight.lastEmail', em); } catch (_) {}
      if (onLogin) onLogin(user);
    } catch (e2) {
      const m = String(e2 && e2.message || e2);
      setErr(m === 'invalid_credentials'
        ? 'Неверный логин или пароль'
        : 'Не удалось войти: ' + m);
      setBusy(false);
    }
  };

  const inputStyle = {
    width:'100%', boxSizing:'border-box', padding:'10px 12px',
    borderRadius:10, border:'1px solid var(--border)',
    background:'var(--bg-sunk)', color:'var(--fg)', font:'inherit', fontSize:14,
  };
  const labelStyle = { display:'flex', flexDirection:'column', gap:4, fontSize:12, color:'var(--fg-muted)' };

  return (
    <div style={{
      position:'fixed', inset:0, background:'var(--bg)',
      display:'flex', alignItems:'center', justifyContent:'center',
      zIndex:1000, padding:20,
    }}>
      <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:14, width:'100%', maxWidth:380}}>
      <form onSubmit={submit} style={{
        width:'100%', maxWidth:380,
        background:'var(--bg-panel)', border:'1px solid var(--border)',
        borderRadius:14, padding:24,
        boxShadow:'0 24px 60px -12px rgba(0,0,0,.22)',
        display:'flex', flexDirection:'column', gap:12,
      }}>
        <div style={{display:'flex', alignItems:'center', gap:10, marginBottom:6}}>
          <Logo size={28}/>
          <div>
            <div style={{fontSize:16, fontWeight:600}}>Автосвет · WMS</div>
            <div style={{fontSize:11, color:'var(--fg-muted)'}}>Вход в систему</div>
          </div>
        </div>
        <label style={labelStyle}>
          Логин
          <input type="text" value={email} onChange={e => setEmail(e.target.value)}
            autoFocus autoComplete="username" placeholder="aleksandr@autosvet.local"
            disabled={busy} style={inputStyle}/>
        </label>
        <label style={labelStyle}>
          Пароль
          <input type="password" value={pwd} onChange={e => setPwd(e.target.value)}
            autoComplete="current-password" disabled={busy} style={inputStyle}/>
        </label>
        <button type="submit" disabled={busy} style={{
          marginTop:4, padding:'11px 14px', borderRadius:10, border:'none',
          background:'var(--accent)', color:'#fff', fontSize:15, fontWeight:600,
          cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.7 : 1,
        }}>
          {busy ? 'Входим…' : 'Войти'}
        </button>
        {err && (
          <div style={{padding:'8px 12px', borderRadius:8, background:'color-mix(in oklch, var(--red) 12%, transparent)', color:'var(--red)', fontSize:12}}>
            {err}
          </div>
        )}
      </form>
        {window.PWAInstallButton && (
          <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:6}}>
            <PWAInstallButton/>
            <div style={{fontSize:11, color:'var(--fg-muted)', textAlign:'center', maxWidth:300, lineHeight:1.4}}>
              Установить как приложение — откроется в отдельном окне на весь экран
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function TopNav({ view, setView, onOpenTweaks, currentUser, onLogout }) {
  const viewMap = {
    manager: 'Главная',
    warehouse: 'Склад',
    cech: 'Цех',
    marketplace: 'Marketplace',
    suppliers: 'Поставщики',
    procurement: 'Снабжение',
    storeman: 'Кладовщик',
    assembler: 'Сборщик',
    scanner: 'Сканер',
    calculator: 'Калькулятор',
    settings: 'Настройки',
  };
  return (
    <div className="topnav">
      <div className="topnav-logo">
        <Logo size={22}/>
        <span>Автосвет <span style={{color:'var(--fg-muted)', fontWeight:500}}>· WMS</span></span>
      </div>
      <div className="topnav-tabs">
        {Object.entries(viewMap).map(([k, label]) => (
          <button key={k} className={`tab ${view===k?'active':''}`} onClick={()=>setView(k)}>{label}</button>
        ))}
      </div>
      <div style={{display:'flex', alignItems:'center', gap:10}}>
        {window.ProcessHistoryButton && <window.ProcessHistoryButton/>}
        {window.PWAInstallButton && <PWAInstallButton/>}
        <button className="btn btn-ghost" style={{padding:6}} onClick={onOpenTweaks}><Icon name="settings"/></button>
        {window.SyncIndicator && <SyncIndicator/>}
        <ProfileMenu currentUser={currentUser} onLogout={onLogout}/>
      </div>
    </div>
  );
}

function TweaksPanel({ tweaks, update, open, onClose }) {
  if (!open) return null;
  return (
    <div className="tweaks-panel open">
      <div style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
        <div style={{fontWeight:600, fontSize:13}}>Tweaks</div>
        <button className="btn btn-ghost" style={{padding:4}} onClick={onClose}><Icon name="x" size={14}/></button>
      </div>
      <div>
        <div className="caps" style={{marginBottom:6}}>Тема</div>
        <div className="role-pill">
          <button className={tweaks.theme==='light'?'active':''} onClick={()=>update({theme:'light'})} style={{flex:1, padding:6}}>Светлая</button>
          <button className={tweaks.theme==='dark'?'active':''} onClick={()=>update({theme:'dark'})} style={{flex:1, padding:6}}>Тёмная</button>
        </div>
      </div>
      <div>
        <div className="caps" style={{marginBottom:6}}>Акцент</div>
        <div style={{display:'flex', gap:6}}>
          {['blue','amber','lime','violet'].map(c => (
            <button key={c} onClick={()=>update({accent:c})} style={{
              flex:1, height:28, borderRadius:8,
              background: (tweaks.theme==='dark'?ACCENT_MAP_DARK:ACCENT_MAP)[c]['--accent'],
              border: tweaks.accent===c ? '2px solid var(--fg)' : '2px solid transparent',
            }}/>
          ))}
        </div>
      </div>
      <div style={{fontSize:11, color:'var(--fg-muted)', paddingTop:6, borderTop:'1px solid var(--border)'}}>
        Изменения сохраняются в localStorage
      </div>
    </div>
  );
}

// Один рабочий iPhone-фрейм с tab-bar'ом, который реально переключает экраны.
// Каждый экземпляр держит собственный tab и chatOpen.
function MobilePhone({ caption, initialTab = 'home', user }) {
  const [tab, setTab] = useStateA(initialTab);
  const [chatOpen, setChatOpen] = useStateA(false);
  const scrollRef = useRefA(null);
  const openChat = () => setChatOpen(true);
  const currentUser = user || EMPLOYEES.find(e => e.id === 'u7') || EMPLOYEES[0];

  // Reset scroll to top on tab switch so the screen doesn't "jump" mid-page.
  useEffectA(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [tab]);

  const screen = (
    <>
      {tab === 'home' && <MobileHome currentUser={currentUser}/>}
      {tab === 'scan' && <MobileScanner/>}
      {tab === 'calc' && <MobileCalculator/>}
      {tab === 'jobs' && <MobileJobs onOpenJob={()=>setTab('assembler')}/>}
      {tab === 'assembler' && <MobileAssembler/>}
    </>
  );

  return (
    <div style={{textAlign:'center'}}>
      <div style={{fontSize:12, color:'var(--fg-muted)', marginBottom:8, fontWeight:500}}>{caption}</div>
      {chatOpen ? (
        <PhoneFrame statusBar={false} noScroll>
          <MobileChat currentUser={currentUser} onClose={()=>setChatOpen(false)}/>
        </PhoneFrame>
      ) : (
        <PhoneFrame
          scrollRef={scrollRef}
          tabBar={<MobileTabBar active={tab === 'assembler' ? 'jobs' : tab} onChange={setTab}/>}
          fab={<ChatLauncher onOpen={openChat} unread={0} positioned={false}/>}
        >
          {screen}
        </PhoneFrame>
      )}
    </div>
  );
}

// Mobile shell — 3 iPhone-фрейма, каждый с рабочим tab-bar'ом и своим дефолтным экраном.
// Hash-параметр #solo=storeman|assembler|procurement переключает на одиночный телефон
// для phone-single.html (открывается в Chrome app-mode 460×940 как «реальный экран»).
const MOBILE_SOLO_PRESETS = {
  storeman:    { caption: 'Кладовщик · Громов В.',  initialTab: 'home',      userId: 'u7' },
  assembler:   { caption: 'Сборщик · Кузьмин В.',   initialTab: 'assembler', userId: 'u9' },
  procurement: { caption: 'Снабженец · Орлов Р.',   initialTab: 'calc',      userId: 'u5' },
};
function MobileShell() {
  const hashParams = new URLSearchParams(location.hash.replace(/^#/, ''));
  const solo = hashParams.get('solo');
  if (solo && MOBILE_SOLO_PRESETS[solo]) {
    const cfg = MOBILE_SOLO_PRESETS[solo];
    return (
      <div style={{display:'flex', justifyContent:'center', padding:'20px 0'}}>
        <MobilePhone caption={cfg.caption} initialTab={cfg.initialTab}
                     user={EMPLOYEES.find(e => e.id === cfg.userId)}/>
      </div>
    );
  }
  return (
    <div style={{display:'flex', gap:24, justifyContent:'center', padding:'20px 0', flexWrap:'wrap', alignItems:'flex-start'}}>
      <MobilePhone caption="Кладовщик · Громов В." initialTab="home"
                   user={EMPLOYEES.find(e => e.id === 'u7')}/>
      <MobilePhone caption="Сборщик · Кузьмин В." initialTab="assembler"
                   user={EMPLOYEES.find(e => e.id === 'u9')}/>
      <MobilePhone caption="Снабженец · Орлов Р." initialTab="calc"
                   user={EMPLOYEES.find(e => e.id === 'u5')}/>
    </div>
  );
}

function App() {
  injectStyles();
  const [tweaks, updateTweaks] = useTweaks();
  const [view, setView] = useStateA(() => localStorage.getItem('autosvet_view') || 'manager');
  const [tweaksOpen, setTweaksOpen] = useStateA(false);

  const [warehouseStep, setWarehouseStep] = useStateA('groups'); // groups | items
  const [warehouseGroup, setWarehouseGroup] = useStateA(null);
  // C1 (2026-08-04): намерение, с которым открыли подгруппу. Сейчас одно —
  // 'add-position': кнопка «Новая позиция» в шапке склада спрашивает подгруппу
  // и сразу открывает форму создания внутри неё. Раньше эта кнопка уводила в
  // Настройки, из-за чего заказчик «ходил два раза».
  const [warehouseGroupIntent, setWarehouseGroupIntent] = useStateA(null);
  const [warehouseScrollTo, setWarehouseScrollTo] = useStateA(null); // id склада (1|2|3) для скролла

  const [openPart, setOpenPart] = useStateA(null);
  const [launchDraft, setLaunchDraft] = useStateA(null);
  const [chatOpen, setChatOpen] = useStateA(false);
  const [chatUnread, setChatUnread] = useStateA(0);

  // Phase 3 — текущий «я» приходит из AUTH (бэкенд /auth/me + /auth/login).
  // authUser хранится в window.AUTH.user; локальный state на компонент
  // — для перерендеринга при auth-changed event.
  // DEV-BYPASS: для просмотра без backend (когда docker compose не поднят)
  // — добавь ?demo=1 в URL или localStorage.setItem('autolight.demoBypass','1').
  // Использовать ТОЛЬКО для локальной разработки на localhost.
  const [authUser, setAuthUser] = useStateA(() => {
    const cur = (window.AUTH && window.AUTH.user) || null;
    if (cur) return cur;
    try {
      const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
      const params = new URLSearchParams(window.location.search);
      const demo = params.has('demo') || localStorage.getItem('autolight.demoBypass') === '1';
      if (isLocal && demo) {
        const mock = { id: 'u-aleksandr', name: 'Александр (demo)', email: 'demo@local', role: 'OWNER' };
        if (window.AUTH) window.AUTH.user = mock;
        return mock;
      }
    } catch (_) {}
    return null;
  });
  useEffectA(() => {
    const onChange = () => setAuthUser((window.AUTH && window.AUTH.user) || null);
    window.addEventListener('auth-changed', onChange);
    return () => window.removeEventListener('auth-changed', onChange);
  }, []);

  // На монтировании: восстанавливаем сессию. Если есть accessToken — /auth/me.
  // Если токена НЕТ (после reload не загрузился / истёк), но юзер ВХОДИЛ
  // (AUTH.user из localStorage) — пробуем refresh по COOKIE, потом пере-резолв
  // роли. Без этого после reload роль залипала на viewer (кнопки серые), т.к.
  // все пути восстановления гейтились на accessToken. (фикс 2026-06-16)
  useEffectA(() => {
    if (!window.AUTH) return;
    if (window.AUTH.accessToken) {
      window.AUTH.me().catch(() => {});
    } else if (window.AUTH.user && typeof window.AUTH._refresh === 'function') {
      window.AUTH._refresh().then(ok => {
        if (ok && window.API && window.API.currentUser) window.API.currentUser(true).catch(() => {});
      }).catch(() => {});
    }
  }, []);

  // Возврат во вкладку/окно после простоя (таймеры heartbeat могли встать при
  // сне ноутбука) — пере-резолвим роль, чтобы права не «слетали» до следующей
  // записи. Дёшево: currentUser кэшируется 60с, форсируем только по фокусу.
  useEffectA(() => {
    const refreshRole = () => {
      if (document.visibilityState === 'hidden') return;
      if (window.API && typeof window.API.currentUser === 'function') {
        window.API.currentUser(true).catch(() => {});
      }
    };
    window.addEventListener('focus', refreshRole);
    document.addEventListener('visibilitychange', refreshRole);
    return () => {
      window.removeEventListener('focus', refreshRole);
      document.removeEventListener('visibilitychange', refreshRole);
    };
  }, []);

  // chatMe — это сотрудник из EMPLOYEES (там роль + initials), мапинг по
  // user.id (он же EMPLOYEES.id благодаря seed-chat.ts). На случай если в
  // EMPLOYEES нет матча — фолбэк synthetic entry из auth-данных.
  const chatMe = (() => {
    if (!authUser) return null;
    const fromList = Array.isArray(EMPLOYEES)
      ? EMPLOYEES.find(e => e.id === authUser.id)
      : null;
    if (fromList) return fromList;
    return {
      id: authUser.id, name: authUser.name || authUser.email,
      email: authUser.email || '', initials: (authUser.name || '??').slice(0,2).toUpperCase(),
      role: authUser.role === 'KEEPER' ? 'storeman'
          : authUser.role === 'DIRECTOR' ? 'superuser'
          : 'viewer',
      active: true,
    };
  })();

  // Live unread badge для FAB. Инкрементируется на любой входящий
  // chat-message пока окно чата закрыто; сброс — при открытии.
  useEffectA(() => {
    if (chatOpen) { setChatUnread(0); return; }
    const onMsg = (e) => {
      const m = e.detail;
      if (!m || !chatMe || m.fromUserId === chatMe.id) return;
      setChatUnread(n => n + 1);
    };
    window.addEventListener('chat-message', onMsg);
    return () => window.removeEventListener('chat-message', onMsg);
  }, [chatOpen, chatMe]);

  // Deep-link параметры для страницы «Поставщики»: какой поставщик и сделка
  // должны быть открыты по клику (из блока «Требуется закуп» на дашборде).
  const [suppliersCtx, setSuppliersCtx] = useStateA(null);
  // Deep-link для «Настройки»: какая вкладка открывается первой
  // (например, «Новая позиция» со склада → вкладка «Группы номенклатуры»).
  const [settingsTab, setSettingsTab] = useStateA('company');

  useEffectA(() => { localStorage.setItem('autosvet_view', view); }, [view]);

  // Header pin — глобальная галочка скрывает шапку (топнав). По умолчанию
  // выключена (шапка видна). При активации body получает класс
  // header-hidden — CSS-правила в styles.jsx коллапсируют топнав до 0
  // высоты, контент под ним занимает освободившееся место. Persist в
  // localStorage между сессиями.
  const [headerHidden, setHeaderHidden] = useStateA(() => {
    try { return localStorage.getItem('autosvet_header_hidden') === '1'; } catch { return false; }
  });
  useEffectA(() => {
    try { localStorage.setItem('autosvet_header_hidden', headerHidden ? '1' : '0'); } catch {}
    document.body.classList.toggle('header-hidden', headerHidden);
    return () => document.body.classList.remove('header-hidden');
  }, [headerHidden]);

  // Warehouse edit-mode — режим редактирования инфраструктуры склада прямо
  // на странице «Склад» (без захода в Настройки → Группы номенклатуры).
  // Включается карандашиком в правом-верхнем углу; виден только для роли
  // owner/admin (см. CURRENT_ROLE / window.CURRENT_USER.role). В режиме
  // редактирования у элементов склада появляются drag-handle, кнопки
  // «+ / ✎ / 🗑»; за пределами режима UI выглядит как обычно.
  // TODO: когда backend для подподгрупп закроется, заменить
  // window.SUBSUBGROUP_OVERRIDES + localStorage('warehouse_order_*') на
  // window.API.subsubgroup.* + window.API.taxonomy.reorder().
  const [warehouseEditMode, setWarehouseEditMode] = useStateA(false);
  useEffectA(() => {
    document.body.classList.toggle('warehouse-editing', warehouseEditMode);
    window.WAREHOUSE_EDIT_MODE = warehouseEditMode;
    try {
      window.dispatchEvent(new CustomEvent('warehouse-edit-mode-changed', { detail: { active: warehouseEditMode } }));
    } catch (_) {}
    return () => document.body.classList.remove('warehouse-editing');
  }, [warehouseEditMode]);
  // Если ушли со страницы склада — гасим edit-режим, чтобы не остаться
  // в нём незаметно на других экранах.
  useEffectA(() => {
    if (view !== 'warehouse' && warehouseEditMode) setWarehouseEditMode(false);
  }, [view, warehouseEditMode]);

  // Demo-режим до бэкенда auth/owner: если CURRENT_USER ещё не приехал
  // (или роль низкая), оверрайдим до 'owner', чтобы карандашик был виден.
  // TODO: убрать после P3 — когда whoami/CanWrite будут давать реальную роль.
  useEffectA(() => {
    if (typeof window.CURRENT_ROLE === 'undefined') {
      window.CURRENT_ROLE = (window.CURRENT_USER && window.CURRENT_USER.role) || 'owner';
    }
  }, []);
  // Решение «показывать ли кнопку»: всем кроме явного viewer. Любая
  // неопределённая роль (auth не пришёл, CURRENT_USER null) → показываем,
  // иначе пользователь думает что фича пропала. Реальные ограничения
  // write-операций — на backend через requireManager.
  const isOwner = (() => {
    const r = (window.CURRENT_USER && window.CURRENT_USER.role) || window.CURRENT_ROLE || 'owner';
    return r !== 'viewer';
  })();

  useEffectA(() => {
    const listener = (e) => {
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', listener);
    try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch {}
    return () => window.removeEventListener('message', listener);
  }, []);

  // Phase 2: SSE-подписка на backend snapshot. Пока backend не существует —
  // EventSource ретраит и SyncIndicator стоит красный, app живёт на localStorage.
  useEffectA(() => {
    if (typeof window.connectStateStream !== 'function') return;
    const es = window.connectStateStream();
    return () => {
      try { es && es.close && es.close(); } catch (_) {}
      if (window.API) window.API._es = null;
    };
  }, []);

  // Phase 2 / A5: подтянуть текущего пользователя и роль из /api/whoami.
  // До этого момента CanWrite-гарды держат write-кнопки заблокированными
  // (default role=viewer). После ответа — dispatch 'current-user-changed'
  // ребутает CanWrite-обёртки.
  useEffectA(() => {
    if (window.API && typeof window.API.currentUser === 'function') {
      window.API.currentUser().catch(() => {});
    }
  }, []);

  const handleGoWarehouse = (wId) => {
    setView('warehouse');
    setWarehouseStep('groups');
    setWarehouseScrollTo(typeof wId === 'number' ? wId : null);
  };
  const handleGoMarketplace = () => setView('marketplace');

  // Универсальный навигатор для KPI-карточек
  const handleNavigate = (target, opts) => {
    if (target === 'warehouse') return handleGoWarehouse(opts?.warehouseId);
    if (target === 'marketplace') return handleGoMarketplace();
    if (target === 'suppliers') {
      setSuppliersCtx(opts || null);
      setView('suppliers');
      return;
    }
    if (target === 'settings') {
      if (opts?.initialTab) {
        setSettingsTab(opts.initialTab);
        // И запоминаем её ТАМ ЖЕ, где Настройки хранят последнюю открытую
        // вкладку. Иначе переход по кнопке проигрывал сохранённой вкладке:
        // экран Настроек размонтируется при уходе, при возврате читает
        // хранилище заново, и запрошенная вкладка терялась у всех, кто хоть
        // раз заходил в Настройки. Это и есть жалоба «точку добавления не
        // находишь с первого раза». Явный переход — это тоже смена вкладки,
        // так что запоминать его правильно.
        try { localStorage.setItem('autolight_settings_tab_v1', opts.initialTab); } catch (_) {}
      }
      setView('settings');
      return;
    }
    setView(target);
  };

  // До логина — закрываем всё приложение модалкой. Никакой fetch к chat / state
  // не нужен без identity. Backend chat-routes требуют auth (Bearer/X-API-Key).
  if (!authUser) {
    return (
      <ToastProvider>
        <LoginGate onLogin={() => setAuthUser(window.AUTH && window.AUTH.user)}/>
      </ToastProvider>
    );
  }

  return (
    <ToastProvider>
      <button
        className={'header-pin-btn' + (headerHidden ? ' active' : '')}
        title={headerHidden ? 'Закрепить шапку' : 'Скрыть шапку наверх'}
        aria-pressed={headerHidden}
        onClick={() => setHeaderHidden(h => !h)}
      >📌</button>
      {view === 'warehouse' && isOwner && (
        <button
          className={'wh-edit-btn' + (warehouseEditMode ? ' active wh-edit-btn--wide' : '')}
          title={warehouseEditMode ? 'Сохранить и выйти из редактирования' : 'Редактировать склад'}
          aria-pressed={warehouseEditMode}
          onClick={() => setWarehouseEditMode(v => !v)}
        >
          {warehouseEditMode
            ? <><Icon name="check" size={14}/> <span style={{marginLeft:6, fontWeight:600}}>Сохранить</span></>
            : <Icon name="edit" size={16}/>
          }
        </button>
      )}
      <TopNav view={view} setView={setView} onOpenTweaks={()=>setTweaksOpen(true)} currentUser={chatMe} onLogout={() => window.AUTH && window.AUTH.logout()}/>
      <div className="app" data-screen-label={view}>
        {view === 'manager' && (
          <DashboardScreen
            onOpenPart={setOpenPart}
            onOpenWarehouse={handleGoWarehouse}
            onGoToWarehouse={handleGoWarehouse}
            onGoToMarketplace={handleGoMarketplace}
            onNavigate={handleNavigate}
            onLaunch={setLaunchDraft}
            userRole={tweaks.role || 'manager'}
          />
        )}
        {view === 'marketplace' && window.MarketplacePage && (
          <MarketplacePage onBack={()=>setView('manager')}/>
        )}
        {view === 'suppliers' && window.SuppliersPage && (
          <SuppliersPage
            onBack={()=>setView('manager')}
            openSupplierId={suppliersCtx?.openSupplierId}
            openDealId={suppliersCtx?.openDealId}
          />
        )}
        {view === 'warehouse' && warehouseStep === 'groups' && (
          <WarehouseGroupsScreen
            onOpenGroup={(gid, intent)=>{setWarehouseGroup(gid); setWarehouseGroupIntent(intent || null); setWarehouseStep('items');}}
            scrollToWarehouseId={warehouseScrollTo}
            onScrolled={()=>setWarehouseScrollTo(null)}
            onNavigate={handleNavigate}
            editMode={warehouseEditMode}
          />
        )}
        {view === 'warehouse' && warehouseStep === 'items' && (
          <GroupItemsScreen
            groupId={warehouseGroup}
            onBack={()=>{setWarehouseGroupIntent(null); setWarehouseStep('groups');}}
            onOpenPart={setOpenPart}
            editMode={warehouseEditMode}
            intent={warehouseGroupIntent}
            onIntentDone={()=>setWarehouseGroupIntent(null)}
          />
        )}
        {view === 'procurement' && <ProcurementDashboard onOpenPart={setOpenPart}/>}
        {view === 'storeman' && <StoremanDashboard onOpenPart={setOpenPart} onOpenWarehouse={handleGoWarehouse}/>}
        {view === 'assembler' && <AssemblerDashboard onOpenPart={setOpenPart}/>}
        {view === 'cech' && window.CechScreen && (
          <CechScreen onGoToMain={() => setView('manager')}/>
        )}
        {view === 'scanner' && <ScannerScreen/>}
        {view === 'calculator' && <CalculatorScreen/>}
        {view === 'settings' && <SettingsScreen currentUser={EMPLOYEES[0] || BOOTSTRAP_USER} initialTab={settingsTab}/>}
      </div>

      {/* Drawer & modals */}
      <ComponentDrawer part={openPart} onClose={()=>setOpenPart(null)}/>
      {launchDraft && <LaunchFlow draft={launchDraft} onClose={()=>setLaunchDraft(null)} onLaunched={()=>{}}/>}

      {/* Chat — FAB на всех десктоп-вью (Главная, Склад, Marketplace,
          Поставщики, Снабжение, Кладовщик, Сборщик, Сканер, Калькулятор,
          Настройки). На 'mobile' не показываем — там MobileShell сам
          включает MobileChat внутри iPhone-фрейма. */}
      {view !== 'mobile' && !chatOpen && (
        <div style={{position:'fixed', right:0, bottom:0, width:'100%', height:'100%', pointerEvents:'none', zIndex:90}}>
          <div style={{position:'relative', width:'100%', height:'100%'}}>
            <div style={{pointerEvents:'auto'}}>
              <ChatLauncher onOpen={()=>setChatOpen(true)} unread={chatUnread}/>
            </div>
          </div>
        </div>
      )}
      {chatOpen && (
        <div style={{position:'fixed', right:0, bottom:0, width:'100%', height:'100%', pointerEvents:'none', zIndex:90}}>
          <div style={{position:'relative', width:'100%', height:'100%', pointerEvents:'auto'}}>
            <DesktopChat onClose={()=>setChatOpen(false)} currentUser={chatMe}/>
          </div>
        </div>
      )}

      <TweaksPanel tweaks={tweaks} update={updateTweaks} open={tweaksOpen} onClose={()=>setTweaksOpen(false)}/>
    </ToastProvider>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
