// Version: 2.02.00
// --- GLOBAL SHARED COMPONENTS ---
const { useState, useEffect, useMemo, useRef } = React;
const ADMIN_VERSION = "2.02.00";
const API_BASE = (window.location.protocol === 'blob:' || window.location.protocol === 'file:') ? 'https://kp-generator-b4fb.onrender.com' : '';
window.TelegramLogin = ({ onAuth, botUsername }) => {
const ref = useRef(null);
const onAuthRef = useRef(onAuth);
useEffect(() => { onAuthRef.current = onAuth; }, [onAuth]);
useEffect(() => {
window.onTelegramAuth = (user) => {
if (onAuthRef.current) onAuthRef.current(user);
};
if (ref.current) {
ref.current.innerHTML = "";
const script = document.createElement("script");
script.src = "https://telegram.org/js/telegram-widget.js?22";
script.setAttribute("data-telegram-login", botUsername);
script.setAttribute("data-size", "large");
script.setAttribute("data-radius", "10");
script.setAttribute("data-request-access", "write");
script.setAttribute("data-userpic", "false");
script.setAttribute("data-onauth", "onTelegramAuth(user)");
script.async = true;
ref.current.appendChild(script);
}
}, [botUsername]);
return
;
};
window.ApiTestButton = () => {
const [status, setStatus] = useState('idle');
const test = async () => {
setStatus('loading');
try {
const res = await fetch(`/api/proxy/search?article=102529M`);
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
if (json.found !== undefined) { setStatus('success'); setTimeout(() => setStatus('idle'), 3000); }
else { throw new Error("Некорректный ответ"); }
} catch (e) { console.error(e); setStatus('error'); setTimeout(() => setStatus('idle'), 3000); }
};
if (status === 'loading') return Проверка... ;
if (status === 'success') return ✅ OK ;
if (status === 'error') return ❌ Ошибка ;
return Тест ;
};
window.BonusBanner = ({ refLink, isVip }) => {
const [expanded, setExpanded] = useState(false);
const copyRef = (e) => {
e.stopPropagation();
navigator.clipboard.writeText(refLink).then(() => alert("Ссылка скопирована!"));
};
const shareTelegram = (e) => {
e.stopPropagation();
const text = encodeURIComponent("Привет! Рекомендую этот конструктор КП. Регистрируйся по ссылке и получи бонусы:");
window.open(`https://t.me/share/url?url=${refLink}&text=${text}`, '_blank');
};
return (
setExpanded(!expanded)} className={`bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl p-4 shadow-lg text-white mb-8 relative overflow-hidden cursor-pointer transition-all ${expanded ? 'h-auto' : 'h-16 flex items-center'}`}>
{isVip ? "Пригласить коллегу" : "Бонусная программа"}
{expanded && (
{isVip ?
"У вас уже максимальный статус VIP. Поделитесь ссылкой с коллегами, чтобы они получили 2 месяца VIP при регистрации!" :
Пригласите коллегу или друга! Вы получите 3 месяца VIP (Speed) , а он — 2 месяца VIP сразу после регистрации.
}
{refLink || "Loading..."}
Копировать
✈️ Поделиться
)}
{expanded &&
}
);
};
window.TariffModal = ({ isOpen, onClose, botLink, prices }) => {
if (!isOpen) return null;
return (
e.stopPropagation()}>
Выберите тариф
Manager
{prices.manager} ⭐️
5 документов
Без водяного знака
Сохранение в облако
Без интеграции 1С
Выбрать Manager
Speed
HIT
{prices.speed} ⭐️
Безлимит документов
Без водяного знака
Интеграция 1С
Приоритетная поддержка
Выбрать Speed
);
};
window.AdminPanel = ({ onBack, adminVersion = ADMIN_VERSION }) => {
const [users, setUsers] = useState([]);
const [tab, setTab] = useState('users');
const [search, setSearch] = useState('');
const [sort, setSort] = useState({ key: 'id', dir: 'desc' });
const [config, setConfig] = useState({ trial_days: 0, price_2_weeks: 0, price_1_month: 0, price_manager_month: 0 });
const [loading, setLoading] = useState(false);
const [userFiles, setUserFiles] = useState(null);
useEffect(() => { loadUsers(); }, []);
const loadUsers = async () => {
setLoading(true);
try {
const res = await fetch('/api/admin/users');
const data = await res.json();
if (!res.ok) { alert(`Error: ${data.message}`); setLoading(false); return; }
if (Array.isArray(data)) setUsers(data);
} catch (e) { console.error(e); }
setLoading(false);
};
const loadConfig = async () => { try { const res = await fetch('/api/admin/config'); if (res.ok) setConfig(await res.json()); } catch (e) { } };
const saveConfig = async () => { try { await fetch('/api/admin/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); alert("Настройки сохранены"); } catch (e) { alert("Ошибка сохранения"); } };
const toggleBlock = async (id) => { if (!confirm("Изменить блокировку?")) return; await fetch(`/api/admin/users/${id}/toggle_block`, { method: 'POST' }); loadUsers(); };
const toggleVip = async (id) => { if (!confirm("Переключить VIP?")) return; await fetch(`/api/admin/users/${id}/toggle_vip`, { method: 'POST' }); loadUsers(); };
const cycleTier = async (id) => { if (!confirm("Изменить тариф (Free -> Manager -> Speed)?")) return; await fetch(`/api/admin/users/${id}/cycle_tier`, { method: 'POST' }); loadUsers(); };
const setSubDays = async (id) => {
const daysStr = prompt("Введите количество дней Speed (0 - чтобы отключить):", "30");
if (daysStr === null) return;
const days = parseInt(daysStr);
if (isNaN(days) || days < 0) { alert("Введите корректное число"); return; }
try {
await fetch(`/api/admin/users/${id}/set_sub`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ days }) });
loadUsers();
} catch (e) { alert("Ошибка обновления"); }
};
const openFiles = async (u) => { setUserFiles({ userId: u.id, name: u.name, files: [] }); try { const res = await fetch(`/api/projects/${u.id}`); if (res.ok) setUserFiles({ userId: u.id, name: u.name, files: await res.json() }); } catch (e) { } };
const downloadFile = async (fid, fname) => { const res = await fetch(`/api/project/${fid}`); const data = await res.json(); const blob = new Blob([data.data], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = (fname || "project") + ".json"; a.click(); };
const deleteFile = async (fid) => { if (!confirm("Удалить файл?")) return; await fetch(`/api/projects/${fid}`, { method: 'DELETE' }); openFiles({ id: userFiles.userId, name: userFiles.name }); loadUsers(); };
const uploadFile = async (e) => { const file = e.target.files[0]; if (!file || !userFiles) return; const reader = new FileReader(); reader.onload = async (ev) => { await fetch(`/api/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userFiles.userId, id: 0, name: file.name.replace('.json', '') + " (Upload)", data: e.target.result }) }); alert("Загружено"); openFiles({ id: userFiles.userId, name: userFiles.name }); loadUsers(); }; reader.readAsText(file); e.target.value = ''; };
const sortedUsers = useMemo(() => {
let res = users.filter(u => (u.name && u.name.toLowerCase().includes(search.toLowerCase())) || (u.username && u.username.toLowerCase().includes(search.toLowerCase())) || String(u.telegram_id).includes(search));
return res.sort((a, b) => {
let valA = a[sort.key], valB = b[sort.key];
if (sort.key === 'created_at' || sort.key === 'last_active') { const parse = (s) => { if (!s) return 0; const [d, t] = s.split(' '); const [dd, mm, yy] = d.split('.'); return new Date(yy, mm - 1, dd).getTime(); }; valA = parse(valA); valB = parse(valB); }
if (valA < valB) return sort.dir === 'asc' ? -1 : 1; if (valA > valB) return sort.dir === 'asc' ? 1 : -1; return 0;
});
}, [users, search, sort]);
const handleSort = (key) => setSort({ key, dir: sort.key === key && sort.dir === 'asc' ? 'desc' : 'asc' });
return (
Админ-панель
Frontend: v{adminVersion}
На сайт
{ setTab('users'); loadUsers() }} className={`px-4 py-2 rounded-md font-bold text-sm ${tab === 'users' ? 'bg-white text-blue-600 shadow' : 'text-gray-500'}`}>Пользователи
{ setTab('settings'); loadConfig() }} className={`px-4 py-2 rounded-md font-bold text-sm ${tab === 'settings' ? 'bg-white text-blue-600 shadow' : 'text-gray-500'}`}>Настройки
{tab === 'users' && (
)}
{tab === 'users' && (
handleSort('name')}>Пользователь {sort.key === 'name' && (sort.dir === 'asc' ? '↑' : '↓')}
Тариф
handleSort('projects_count')}>Проекты {sort.key === 'projects_count' && (sort.dir === 'asc' ? '↑' : '↓')}
handleSort('storage_kb')}>Объем {sort.key === 'storage_kb' && (sort.dir === 'asc' ? '↑' : '↓')}
handleSort('last_active')}>Активность {sort.key === 'last_active' && (sort.dir === 'asc' ? '↑' : '↓')}
handleSort('created_at')}>Регистрация {sort.key === 'created_at' && (sort.dir === 'asc' ? '↑' : '↓')}
Файлы
Действия
{loading ? Загрузка... :
sortedUsers.map(u => {
const tierStyle = u.is_vip ? 'bg-yellow-100 text-yellow-700 border-yellow-200' :
u.tier === 'speed' ? 'bg-purple-100 text-purple-700 border-purple-200' :
u.tier === 'manager' ? 'bg-blue-50 text-blue-600 border-blue-200' :
'bg-gray-100 text-gray-500';
return (
{u.photo ? : {u.name?.[0]}
}
cycleTier(u.id)} className={`px-2 py-1 rounded text-xs font-bold border ${tierStyle}`} title="Переключить тариф">{u.sub_status}
setSubDays(u.id)} className="text-gray-400 hover:text-blue-600" title="Изменить дни Speed">
{u.projects_count}
{u.storage_kb} KB
{u.last_active}
{u.created_at}
openFiles(u)} className="text-blue-600 hover:underline text-xs font-bold">Открыть
toggleVip(u.id)} className={`p-1 rounded ${u.is_vip ? 'text-yellow-500 bg-yellow-50' : 'text-gray-300 hover:bg-gray-100'}`} title="VIP">
toggleBlock(u.id)} className={`px-2 py-1 rounded text-xs font-bold border ${u.is_blocked ? 'border-green-200 text-green-600' : 'border-red-200 text-red-500'}`}>{u.is_blocked ? 'Разблок' : 'Блок'}
)
})}
)}
{tab === 'settings' && (
)}
{userFiles && (
setUserFiles(null)}>
e.stopPropagation()}>
Файлы: {userFiles.name}
setUserFiles(null)}>
{userFiles.files.length === 0 ? (
Нет файлов
) : (
userFiles.files.map(f => (
{f.name}
{new Date(f.updated_at).toLocaleString()}
downloadFile(f.id, f.name)} className="p-1.5 text-blue-600 hover:bg-blue-100 rounded">
deleteFile(f.id)} className="p-1.5 text-red-500 hover:bg-red-100 rounded">
))
)}
)}
);
};