// Version: 2.11.67 (QR Proxy & Link Copy Failsafe) var { useState, useEffect, useRef } = React; const SafePage = ({ name, ...props }) => { const Component = window[name]; if (!Component) { return (
⏳ Ожидание загрузки {name}...
); } return ; }; var SafeIcon = ({ name, fallback = 'LayoutTemplate', ...props }) => { let Icon = window[name]; if (!Icon) Icon = window[fallback]; if (!Icon) return [X]; return ; }; window.SafeIcon = SafeIcon; // --- МОБИЛЬНЫЙ ЭКРАН ПОДТВЕРЖДЕНИЯ --- window.MobileAuth = ({ sessionId }) => { const [code, setCode] = useState(null); const [error, setError] = useState(null); useEffect(() => { const getCode = async () => { try { // Создаем простой слепок браузера const fingerprint = navigator.userAgent + window.screen.width + window.screen.height; const res = await fetch('/api/auth/mobile_confirm', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ session_id: sessionId, fingerprint }) }); if (res.ok) { const data = await res.json(); setCode(data.code); } else { setError("Сессия устарела или сканирование не удалось."); } } catch(e) { setError("Ошибка сети. Проверьте подключение к интернету."); } }; getCode(); }, [sessionId]); if (error) { return (

Ошибка

{error}

); } if (!code) { return (

Проверка устройства...

); } return (

Ваш код для входа

Введите эти 4 цифры на компьютере, чтобы подтвердить авторизацию.

{code}
Устройство проверено
); }; // ... existing RequisitesModal, SupportModal components ... window.RequisitesModal = ({ isOpen, onClose, user, onUpdateUser }) => { const [logo, setLogo] = useState(''); const [reqs, setReqs] = useState(''); const [loading, setLoading] = useState(false); const [isDragging, setIsDragging] = useState(false); useEffect(() => { if (isOpen && user) { setLogo(user.company_logo || ''); setReqs(user.company_requisites || ''); } }, [isOpen, user]); const uploadFile = async (file) => { if (!file) return; const formData = new FormData(); formData.append('file', file); setLoading(true); try { const res = await fetch('/api/upload/image', { method: 'POST', body: formData }); const data = await res.json(); if (data.url) setLogo(data.url); } catch (err) { alert('Ошибка загрузки'); } setLoading(false); }; const handleUpload = (e) => uploadFile(e.target.files[0]); const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); setIsDragging(false); }; const handleDrop = (e) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { uploadFile(file); } }; const handleSave = async () => { setLoading(true); try { await fetch(`/api/user/${user.id}/requisites`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ logo: logo, requisites: reqs }) }); onUpdateUser({ ...user, company_logo: logo, company_requisites: reqs }); onClose(); } catch (e) { alert("Ошибка сохранения"); } setLoading(false); }; if (!isOpen) return null; return (
e.stopPropagation()}>

Мои реквизиты

{logo ? (
Логотип
) : ( )}
); }; window.SupportModal = ({ isOpen, onClose, user }) => { const [tab, setTab] = useState('new'); const [tickets, setTickets] = useState([]); const [activeTicket, setActiveTicket] = useState(null); const [chatData, setChatData] = useState(null); const [type, setType] = useState('tech'); const [text, setText] = useState(''); const [imageUrl, setImageUrl] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { if (isOpen && user) loadTickets(); }, [isOpen, user]); useEffect(() => { let interval; if (isOpen && tab === 'chat' && activeTicket) { interval = setInterval(() => loadChat(activeTicket), 5000); } return () => clearInterval(interval); }, [isOpen, tab, activeTicket]); const loadTickets = async () => { try { const res = await fetch(`/api/support/tickets/${user.id}`); const data = await res.json(); setTickets(data); if (data.length > 0 && tab === 'new' && !text) setTab('list'); } catch (e) { console.error(e); } }; const loadChat = async (id) => { try { const res = await fetch(`/api/support/tickets/${user.id}/${id}`); const data = await res.json(); setChatData(data); } catch (e) { console.error(e); } }; const openChat = (id) => { setActiveTicket(id); setTab('chat'); setChatData(null); loadChat(id); }; const handleImageUpload = async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); setLoading(true); try { const res = await fetch('/api/upload/image', { method: 'POST', body: formData }); const data = await res.json(); if (data.url) setImageUrl(data.url); } catch (err) { alert('Ошибка загрузки фото'); } setLoading(false); }; const submitNewTicket = async () => { if (!text.trim()) { alert("Введите текст"); return; } setLoading(true); try { const res = await fetch('/api/support/tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: user.id, type, text, image_url: imageUrl }) }); const data = await res.json(); if (data.status === 'ok') { setText(''); setImageUrl(''); await loadTickets(); openChat(data.ticket_id); } } catch (e) { alert("Ошибка отправки"); } setLoading(false); }; const submitMessage = async () => { if (!text.trim() && !imageUrl) return; setLoading(true); try { await fetch(`/api/support/tickets/${activeTicket}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sender: 'user', text, image_url: imageUrl }) }); setText(''); setImageUrl(''); await loadChat(activeTicket); } catch (e) { alert("Ошибка отправки"); } setLoading(false); }; if (!isOpen) return null; return (
e.stopPropagation()}>

Поддержка

{tab === 'new' && (
{imageUrl ? (
) : ( )}
)} {tab === 'list' && (
{tickets.length === 0 ?
У вас пока нет обращений
: tickets.map(t => (
openChat(t.id)} className="border border-gray-200 rounded-xl p-4 cursor-pointer hover:bg-gray-50 transition relative">
{t.type==='tech'?'Вопрос':'Идея'} #{t.id} {t.status==='closed'?'Закрыт':'Открыт'}
{t.last_message || '...'}
{t.updated_at}
{t.has_unread &&
}
)) }
)} {tab === 'chat' && chatData && (
Статус: {chatData.status === 'closed' ? 'Закрыт' : 'Открыт'}
{chatData.messages.map(m => (
{m.image && }
{m.text}
{m.sender==='admin'?'Служба поддержки':user.name} • {m.time}
))}
{chatData.status === 'open' ? (
{imageUrl && (
)}
) : (
Обращение закрыто. Для новых вопросов создайте новую заявку.
)}
)}
); }; window.App = () => { const [view, setView] = useState('loading'); const [user, setUser] = useState(null); const [botLink, setBotLink] = useState(''); const [projects, setProjects] = useState([]); const [currentProject, setCurrentProject] = useState(null); const [prices, setPrices] = useState({ manager: 150, speed: 250, enable_furniture: false, global_announcement: '', announcement_exclude_vip: true }); const [useAquaPlaza, setUseAquaPlaza] = useState(true); const [sessionToken, setSessionToken] = useState(localStorage.getItem('kp_session_token') || ''); // Auth State (PC) const [authStep, setAuthStep] = useState(1); // 1: phone, 2: qr, 3: code const [phoneInput, setPhoneInput] = useState(''); const [authSessionId, setAuthSessionId] = useState(''); const [authCode, setAuthCode] = useState(''); const [qrUrl, setQrUrl] = useState(''); const [qrImageBase64, setQrImageBase64] = useState(''); // NEW: Хранит саму картинку QR const [authError, setAuthError] = useState(''); // Admin backdoor const [showAdminLogin, setShowAdminLogin] = useState(false); const [adminUser, setAdminUser] = useState(''); const [adminPass, setAdminPass] = useState(''); const [adminLoading, setAdminLoading] = useState(false); const handleLogout = () => { localStorage.removeItem('kp_user'); localStorage.removeItem('kp_auth_date'); localStorage.removeItem('kp_session_token'); setSessionToken(''); setUser(null); setAuthStep(1); setPhoneInput(''); setAuthCode(''); setQrImageBase64(''); setView('landing'); }; // Восстановление сессии const refreshUserStatus = async (currentUser, currentToken) => { if (!currentUser) return null; try { const res = await fetch(`/api/user/status/${currentUser.id}?token=${currentToken || 'INVALID'}&t=${Date.now()}`); if (res.status === 401) { const err = await res.json(); if (err.status === 'session_expired') { alert("Выполнен вход с другого устройства. Ваша сессия завершена."); handleLogout(); return null; } } if (res.ok) { const status = await res.json(); const updatedUser = { ...currentUser, is_premium: status.is_premium, is_vip: status.is_vip, tier: status.tier, sub_end: status.sub_end, days_left: status.days_left, total_invites: status.total_invites, paid_invites: status.paid_invites, unread_support: status.unread_support, company_logo: status.company_logo !== undefined ? status.company_logo : currentUser.company_logo, company_requisites: status.company_requisites !== undefined ? status.company_requisites : currentUser.company_requisites }; setUser(updatedUser); localStorage.setItem('kp_user', JSON.stringify(updatedUser)); return updatedUser; } } catch (e) { console.error("Failed to refresh user status", e); } return currentUser; }; // Инициализация при загрузке приложения (проверка URL и восстановление сессии) useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const mode = urlParams.get('mode'); const sessId = urlParams.get('session_id'); const startParam = urlParams.get('start'); // 1. Проверяем, не открыт ли сайт с мобилки по ссылке из QR-кода if (mode === 'mobile_auth' && sessId) { setView('mobile_auth'); setAuthSessionId(sessId); return; // Прерываем проверку сессии, чтобы не сбросить мобильный экран } // 2. Обычный режим (ПК или прямой заход). Проверяем сохраненную сессию const savedUser = localStorage.getItem('kp_user'); const savedAuthDate = localStorage.getItem('kp_auth_date'); const savedSetting = localStorage.getItem('use_aquaplaza'); if (savedSetting === 'false') setUseAquaPlaza(false); const now = Date.now(); const oneDay = 24 * 60 * 60 * 1000; if (savedUser && savedAuthDate && (now - parseInt(savedAuthDate) < oneDay)) { try { const u = JSON.parse(savedUser); refreshUserStatus(u, sessionToken).then((freshUser) => { if (freshUser) { setUser(freshUser); setView('dashboard'); fetchProjects(freshUser.id); } }); fetch('/api/config/public').then(r => r.json()).then(d => { if(d) setPrices(prev => ({ ...prev, manager: d.price_manager, speed: d.price_speed, enable_furniture: d.enable_furniture, global_announcement: d.global_announcement || '', announcement_exclude_vip: d.announcement_exclude_vip !== undefined ? d.announcement_exclude_vip : true })); }).catch(e => console.error("Price fetch error", e)); } catch (e) { localStorage.removeItem('kp_user'); if (startParam) setView('auth'); else setView('landing'); } } else { localStorage.removeItem('kp_user'); localStorage.removeItem('kp_auth_date'); if (startParam) { setView('auth'); } else { setView('landing'); } } }, []); // <-- Пустой массив зависимостей, срабатывает 1 раз! // Polling для ПК во время шага QR useEffect(() => { let interval; if (view === 'auth' && authStep === 2 && authSessionId) { interval = setInterval(async () => { try { const res = await fetch(`/api/auth/qr_poll/${authSessionId}`); if (res.ok) { const data = await res.json(); if (data.status === 'scanned') { setAuthStep(3); // Переключаем на ввод 4-значного кода } } } catch (e) { console.error("Poll err", e); } }, 2000); } return () => clearInterval(interval); }, [view, authStep, authSessionId]); useEffect(() => { let interval; if (view === 'dashboard' && user) { interval = setInterval(() => refreshUserStatus(user, sessionToken), 10000); } return () => clearInterval(interval); }, [view, user, sessionToken]); const toggleAquaPlaza = () => { const newValue = !useAquaPlaza; setUseAquaPlaza(newValue); localStorage.setItem('use_aquaplaza', newValue.toString()); }; const fetchProjects = async (uid) => { try { const res = await fetch(`/api/projects/${uid}?t=${Date.now()}`); const data = await res.json(); setProjects(Array.isArray(data) ? data : []); } catch (e) { setProjects([]); } }; const loadProject = async (pid) => { try { const res = await fetch(`/api/project/${pid}`); if (res.ok) { const data = await res.json(); const parsedData = JSON.parse(data.data); setCurrentProject({ id: data.id, data: parsedData, name: data.name }); if (parsedData.templateType === 'furniture') setView('furniture_editor'); else setView('editor'); } } catch (e) { alert("Ошибка загрузки"); } }; const deleteProject = async (e, pid) => { e.stopPropagation(); if (!confirm("Удалить проект?")) return; try { await fetch(`/api/projects/${pid}`, { method: 'DELETE' }); fetchProjects(user.id); } catch (e) { alert("Ошибка удаления"); } }; // === ФУНКЦИИ НОВОГО ЛОГИНА (PC) === const handlePhoneSubmit = async () => { const cleanPhone = phoneInput.replace(/\D/g, ''); // Скрытый вход для админа по номеру if (cleanPhone === '79253115397') { setShowAdminLogin(true); setPhoneInput(''); // очистим поле ввода return; } if (cleanPhone.length < 10) { setAuthError('Введите корректный номер телефона'); return; } setAuthError(''); try { const res = await fetch('/api/auth/qr_init', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ phone: cleanPhone }) }); if (res.ok) { const data = await res.json(); setAuthSessionId(data.session_id); // Формируем ссылку для QR кода (ведет на наш же сайт с параметрами) const currentDomain = window.location.origin; const link = `${currentDomain}/?mode=mobile_auth&session_id=${data.session_id}`; setQrUrl(link); setAuthStep(2); setQrImageBase64(''); // Очищаем старую картинку, если была // Запрашиваем саму картинку QR-кода через наш бэкенд прокси, чтобы обойти AdBlock const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(link)}`; if (window.urlToBase64) { const b64 = await window.urlToBase64(qrApiUrl); if (b64) setQrImageBase64(b64); } } else { setAuthError('Ошибка инициализации сессии'); } } catch (e) { setAuthError('Ошибка сети'); } }; const handleCodeSubmit = async () => { if (authCode.length !== 4) { setAuthError('Код должен состоять из 4 цифр'); return; } setAuthError(''); try { const res = await fetch('/api/auth/qr_verify', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ session_id: authSessionId, code: authCode }) }); const data = await res.json(); if (res.ok && data.status === 'ok') { setUser(data.user); if (data.bot_link) setBotLink(data.bot_link); if (data.config) setPrices({ manager: data.config.price_manager, speed: data.config.price_speed, enable_furniture: data.config.enable_furniture, global_announcement: data.config.global_announcement || '', announcement_exclude_vip: data.config.announcement_exclude_vip !== undefined ? data.config.announcement_exclude_vip : true }); localStorage.setItem('kp_user', JSON.stringify(data.user)); localStorage.setItem('kp_auth_date', Date.now().toString()); localStorage.setItem('kp_session_token', data.token); setSessionToken(data.token); await fetchProjects(data.user.id); setView('dashboard'); } else { setAuthError(data.detail || 'Неверный код. Попробуйте еще раз.'); } } catch (e) { setAuthError('Ошибка проверки кода'); } }; const handleAdminLogin = async () => { if(!adminUser || !adminPass) return; setAdminLoading(true); try { const res = await fetch('/api/auth/admin_login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: adminUser, password: adminPass }) }); const data = await res.json(); if(res.ok && data.status === 'ok') { setUser(data.user); if (data.bot_link) setBotLink(data.bot_link); if (data.config) setPrices({ manager: data.config.price_manager, speed: data.config.price_speed, enable_furniture: data.config.enable_furniture, global_announcement: data.config.global_announcement || '', announcement_exclude_vip: data.config.announcement_exclude_vip !== undefined ? data.config.announcement_exclude_vip : true }); localStorage.setItem('kp_user', JSON.stringify(data.user)); localStorage.setItem('kp_auth_date', Date.now().toString()); localStorage.setItem('kp_session_token', data.token); setSessionToken(data.token); await fetchProjects(data.user.id); setShowAdminLogin(false); setView('dashboard'); } else { alert("Ошибка входа: " + (data.detail || "Неверный логин или пароль")); } } catch (e) { alert("Сетевая ошибка"); } setAdminLoading(false); }; const renderAppContent = () => { if (view === 'loading') return
; // Экран подтверждения на смартфоне if (view === 'mobile_auth') return ; if (view === 'editor') return { setView('dashboard'); fetchProjects(user.id); }} />; if (view === 'furniture_editor') return { setView('dashboard'); fetchProjects(user.id); }} />; if (view === 'landing') { return {setAuthStep(1); setView('auth');}} />; } if (view === 'auth') { return (

Безопасный Вход

{/* ШАГ 1: ВВОД ТЕЛЕФОНА */} {authStep === 1 && (

Введите номер телефона для безопасной идентификации (Identification-First).

{ let val = e.target.value; if (val === '8') val = '+7 '; else if (val === '7') val = '+7 '; else if (val === '9') val = '+7 9'; else if (val.length > 1 && !val.startsWith('+')) { if (val.startsWith('8')) val = '+7 ' + val.slice(1); else if (val.startsWith('7')) val = '+7 ' + val.slice(1); else if (val.startsWith('9')) val = '+7 ' + val; } setPhoneInput(val); }} onKeyDown={(e) => e.key === 'Enter' && handlePhoneSubmit()} className="w-full bg-gray-50 border-2 border-gray-200 rounded-xl px-4 py-4 text-xl font-bold text-center text-gray-800 focus:border-blue-500 focus:bg-white outline-none transition-colors mb-4" /> {authError &&
{authError}
}
)} {/* ШАГ 2: ПОКАЗ QR */} {authStep === 2 && (

Наведите камеру смартфона на QR-код для подтверждения входа.

{qrImageBase64 ? ( QR Code ) : (
Генерация QR...
)}
Ожидаем сканирования...
{/* Дополнительная подстраховка, если QR заблокирован вообще всем чем можно */}
)} {/* ШАГ 3: ВВОД КОДА ИЗ ТЕЛЕФОНА */} {authStep === 3 && (

Устройство проверено. Введите 4-значный код, который появился на экране смартфона.

{ const val = e.target.value.replace(/\D/g, ''); setAuthCode(val); if (val.length === 4) { // Небольшой хак, чтобы дать state обновиться перед вызовом setTimeout(() => document.getElementById('verify-btn').click(), 50); } }} className="w-full bg-gray-50 border-2 border-blue-200 rounded-xl px-4 py-4 text-4xl tracking-[0.5em] font-black text-center text-blue-600 focus:border-blue-500 focus:bg-white outline-none transition-colors mb-4 font-mono" autoFocus /> {authError &&
{authError}
}
)}
setShowAdminLogin(true)} className="cursor-pointer hover:text-gray-400 transition-colors">v2.11.67 | Хост: {window.location.hostname}
{/* Скрытая модалка для логина админа (Backdoor) */} {showAdminLogin && (

Admin Backdoor

setAdminUser(e.target.value)} className="w-full mb-3 p-4 border-2 border-gray-200 rounded-xl focus:border-blue-500 outline-none font-bold" /> setAdminPass(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdminLogin()} className="w-full mb-6 p-4 border-2 border-gray-200 rounded-xl focus:border-blue-500 outline-none font-bold" />
)}
); } return ( refreshUserStatus(user, sessionToken)} onCreateStandard={() => { setCurrentProject(null); setView('editor'); }} onCreateFurniture={() => { setCurrentProject(null); setView('furniture_editor'); }} onLoadProject={loadProject} onDeleteProject={deleteProject} onLogout={handleLogout} onUpdateUser={(newUser) => setUser(newUser)} /> ); }; return ( <> {renderAppContent()} ); }; const Dashboard = ({ user, projects, botLink, prices, sessionToken, useAquaPlaza, onToggleAquaPlaza, onRefreshUser, onCreateStandard, onCreateFurniture, onLoadProject, onDeleteProject, onLogout, onUpdateUser }) => { const [showTariffModal, setShowTariffModal] = useState(false); const [showSupportModal, setShowSupportModal] = useState(false); const [showIntegrations, setShowIntegrations] = useState(false); const [showReqModal, setShowReqModal] = useState(false); const [dismissedAnnouncement, setDismissedAnnouncement] = useState(localStorage.getItem('kp_dismissed_announcement') || ''); const handleDismissAnnouncement = () => { if (prices?.global_announcement) { setDismissedAnnouncement(prices.global_announcement); localStorage.setItem('kp_dismissed_announcement', prices.global_announcement); } }; return (
КП

Конструктор КП

{user.is_vip ? ( VIP (Для своих) ) : (
{user.tier.toUpperCase()} {user.sub_end ? `(до ${user.sub_end})` : ''} {user.tier !== 'speed' && ( )}
)} {user.role === 'admin' && ( )}
{user.name}
{showIntegrations && (
setShowIntegrations(false)}>
e.stopPropagation()}>

Интеграции

Aquaplaza-online.ru
Поиск товаров по артикулу
)}
{prices?.global_announcement && prices.global_announcement !== dismissedAnnouncement && (!user?.is_vip || !prices?.announcement_exclude_vip) && (
{prices.global_announcement}
)}
Создать Товарное КП Классическая таблица
{(user?.role === 'admin' || prices?.enable_furniture) && (
{user?.role === 'admin' && !prices?.enable_furniture && (
Скрыто от юзеров
)}
Создать Мебельное КП Визуал + расчет (ЛДСП и т.д.)
)}

Мои Проекты

{projects.map(p => (
onLoadProject(p.id)} className="kp-card cursor-pointer group bg-white border border-gray-100 rounded-xl p-4 flex items-start gap-3 relative shadow-sm hover:shadow-md transition-all hover:border-blue-200">

{p.name}

Обновлено: {new Date(p.updated_at).toLocaleString('ru-RU', {day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute:'2-digit'})}
))}
setShowReqModal(false)} user={user} onUpdateUser={onUpdateUser} /> {setShowSupportModal(false); onRefreshUser();}} user={user} /> setShowTariffModal(false)} botLink={botLink} prices={prices} user={user} onPaymentSuccess={(updatedUser) => { onUpdateUser(updatedUser); setShowTariffModal(false); }} />
); }; const rootElement = document.getElementById('kp-app-root'); if (rootElement) { const root = ReactDOM.createRoot(rootElement); root.render(); }