Файл: new_top/modules/office/index.php
Строк: 860
<?php
/**
* User profile / office — multifunctional
*/
declare(strict_types=1);
$action = $_GET['action'] ?? 'dashboard';
$userId = (int)($_SESSION['user_id'] ?? 0);
$level = (int)($_SESSION['user_level'] ?? 0);
$cfgSite = Database::fetch('SELECT * FROM config WHERE id = 1') ?: [];
// ---- Auth: login / register ----
if ($userId < 1) {
$mode = ($_GET['mode'] ?? '') === 'reg' ? 'reg' : 'login';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$form = $_POST['form'] ?? 'login';
if ($form === 'login') {
if (!login_throttle_check('office_login')) {
$error = 'Слишком много попыток. Подождите 15 минут.';
} else {
$login = trim($_POST['login'] ?? '');
$pass = $_POST['password'] ?? '';
$user = Database::fetch('SELECT * FROM user WHERE login = ? AND ban = 0', [$login]);
if ($user && password_verify($pass, $user['password'])) {
login_throttle_reset('office_login');
session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['user_level'] = (int)$user['level'];
$_SESSION['user_login'] = $user['login'];
Database::query('UPDATE user SET last_time = ?, ip = ? WHERE id = ?', [time(), client_ip(), $user['id']]);
redirect('/?module=office');
}
login_throttle_hit('office_login');
$error = 'Неверный логин или пароль';
}
} elseif ($form === 'reg') {
if (empty($cfgSite['reg'])) {
$error = 'Регистрация закрыта';
} else {
$login = trim($_POST['login'] ?? '');
$email = trim($_POST['email'] ?? '');
$pass = $_POST['password'] ?? '';
$pass2 = $_POST['password2'] ?? '';
if (mb_strlen($login) < 3 || mb_strlen($login) > 32) $error = 'Логин 3–32 символа';
elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $login)) $error = 'Логин: латиница, цифры, _';
elseif (mb_strlen($pass) < 6) $error = 'Пароль не менее 6 символов';
elseif ($pass !== $pass2) $error = 'Пароли не совпадают';
elseif (Database::fetchColumn('SELECT id FROM user WHERE login = ?', [$login])) $error = 'Логин занят';
else {
$id = Database::insert('user', [
'login' => $login,
'password' => password_hash($pass, PASSWORD_DEFAULT),
'email' => $email,
'level' => 1,
'reg_time' => time(),
'last_time' => time(),
'ip' => client_ip(),
]);
$_SESSION['user_id'] = $id;
$_SESSION['user_level'] = 1;
$_SESSION['user_login'] = $login;
redirect('/?module=office');
}
}
$mode = 'reg';
}
}
?>
<!DOCTYPE html>
<html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<?= pwa_meta() ?>
<title><?= $mode === 'reg' ? 'Регистрация' : 'Вход' ?></title>
<link rel="stylesheet" href="/assets/css/style.css">
</head><body>
<div class="auth-wrap">
<div class="auth-box">
<div class="logo-lg">T</div>
<h1><?= $mode === 'reg' ? 'Регистрация' : 'Личный кабинет' ?></h1>
<?php if ($error): ?><div class="alert alert-error"><?= e($error) ?></div><?php endif; ?>
<?php if ($mode === 'login'): ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="form" value="login">
<label>Логин</label>
<input type="text" name="login" required autofocus autocomplete="username">
<label>Пароль</label>
<input type="password" name="password" required autocomplete="current-password">
<button type="submit" class="btn btn-primary btn-block">Войти</button>
</form>
<?php if (!empty($cfgSite['reg'])): ?>
<p class="text-center" style="margin-top:16px;font-size:.9rem"><a href="/?module=office&mode=reg">Создать аккаунт</a></p>
<?php endif; ?>
<?php else: ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="form" value="reg">
<label>Логин</label>
<input type="text" name="login" required autofocus pattern="[a-zA-Z0-9_]{3,32}">
<label>Email</label>
<input type="email" name="email">
<label>Пароль</label>
<input type="password" name="password" required minlength="6">
<label>Повтор пароля</label>
<input type="password" name="password2" required minlength="6">
<button type="submit" class="btn btn-primary btn-block">Зарегистрироваться</button>
</form>
<p class="text-center" style="margin-top:16px;font-size:.9rem"><a href="/?module=office">Уже есть аккаунт</a></p>
<?php endif; ?>
<p class="text-center text-muted" style="margin-top:14px;font-size:.85rem"><a href="/">← На главную</a></p>
</div>
</div>
<?= pwa_register() ?>
</body></html>
<?php
exit;
}
if ($action === 'logout') {
session_destroy();
redirect('/');
}
$base = site_url();
$flash = $_SESSION['flash'] ?? '';
unset($_SESSION['flash']);
$error = '';
// ---- POST ----
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$do = $_POST['do'] ?? '';
try {
if ($do === 'add') {
$url = sanitize_platform_url((string)($_POST['url'] ?? ''));
$msg = trim($_POST['message'] ?? '');
$cat = (int)($_POST['cat_id'] ?? 13);
$needMod = !empty($cfgSite['pf_mod']) ? 1 : 0;
if ($url === null) {
$error = 'Некорректный URL (только домен, например example.com)';
} elseif (Database::fetchColumn('SELECT id FROM platforms WHERE url = ?', [$url])) {
$error = 'Такой адрес уже есть';
} else {
$styleBig = max(1, min(11, (int)($_POST['style_big'] ?? 1)));
$styleSmall = max(1, min(11, (int)($_POST['style_small'] ?? 1)));
$id = Database::insert('platforms', [
'user_id' => $userId,
'url' => $url,
'message' => mb_substr($msg, 0, 120),
'cat_id' => $cat,
'mode' => $needMod,
'time_add' => time(),
'style_big' => $styleBig,
'style_small' => $styleSmall,
]);
$_SESSION['flash'] = $needMod ? 'Площадка на модерации' : 'Площадка добавлена';
redirect('/?module=office&action=code&id=' . $id);
}
}
if ($do === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$pf = Database::fetch('SELECT * FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
if (!$pf) {
$error = 'Не найдено';
} else {
$url = sanitize_platform_url((string)($_POST['url'] ?? ''));
$msg = mb_substr(trim($_POST['message'] ?? ''), 0, 120);
$cat = (int)($_POST['cat_id'] ?? 13);
$exists = $url ? Database::fetchColumn('SELECT id FROM platforms WHERE url = ? AND id != ?', [$url, $id]) : true;
if ($url === null || $exists) {
$error = 'URL занят или некорректен';
} else {
$styleBig = max(1, min(11, (int)($_POST['style_big'] ?? 1)));
$styleSmall = max(1, min(11, (int)($_POST['style_small'] ?? 1)));
Database::query('UPDATE platforms SET url = ?, message = ?, cat_id = ?, style_big = ?, style_small = ? WHERE id = ? AND user_id = ?',
[$url, $msg, $cat, $styleBig, $styleSmall, $id, $userId]);
$_SESSION['flash'] = 'Сохранено';
redirect('/?module=office&action=platforms');
}
}
}
if ($do === 'delete') {
$id = (int)($_POST['id'] ?? 0);
Database::query('DELETE FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
$_SESSION['flash'] = 'Удалено';
redirect('/?module=office&action=platforms');
}
if ($do === 'buy_gold_points') {
$pid = (int)($_POST['platform_id'] ?? 0);
$days = max(1, min(90, (int)($_POST['days'] ?? 7)));
$res = Points::buyGold($userId, $pid, $days);
if ($res['ok']) {
$_SESSION['flash'] = 'GOLD до ' . date('d.m.Y H:i', (int)$res['until']) . ' (−' . (int)$res['cost'] . ' баллов)';
} else {
$error = $res['error'] ?? 'Ошибка';
}
if (empty($error)) {
redirect('/?module=office&action=points');
}
}
if ($do === 'pay_ad_points') {
$adId = (int)($_POST['ad_id'] ?? 0);
$res = Points::payAd($userId, $adId);
if ($res['ok']) {
$_SESSION['flash'] = 'Реклама оплачена баллами (−' . (int)$res['cost'] . ') и активирована';
redirect('/?module=office&action=ads');
}
$error = $res['error'] ?? 'Ошибка';
}
if ($do === 'buy_ad') {
$pos = trim($_POST['position'] ?? '');
$priceRow = Ads::price($pos);
if (!$priceRow) {
$error = 'Место недоступно';
} else {
$url = sanitize_platform_url((string)($_POST['url'] ?? ''));
$name = mb_substr(trim($_POST['name'] ?? ''), 0, 64);
$text = mb_substr(trim($_POST['text'] ?? ''), 0, 200);
$days = max(1, min(90, (int)($_POST['days'] ?? 7)));
if ($url === null || $name === '') {
$error = 'Укажите корректный URL и название';
} else {
$price = round((float)$priceRow['price_day'] * $days, 2);
Database::insert('ads', [
'position' => $pos,
'user_id' => $userId,
'status' => 0,
'url' => $url,
'name' => $name,
'text' => $text,
'days' => $days,
'price' => $price,
'time_add' => time(),
'time_end' => 0,
'time_pay' => 0,
]);
$_SESSION['flash'] = 'Заявка отправлена. После оплаты администратор активирует рекламу. Сумма: ' . $price . ' ₽';
redirect('/?module=office&action=ads');
}
}
}
if ($do === 'save_styles') {
$id = (int)($_POST['id'] ?? 0);
$styleBig = max(1, min(11, (int)($_POST['style_big'] ?? 1)));
$styleSmall = max(1, min(11, (int)($_POST['style_small'] ?? 1)));
Database::query(
'UPDATE platforms SET style_big = ?, style_small = ? WHERE id = ? AND user_id = ?',
[$styleBig, $styleSmall, $id, $userId]
);
$_SESSION['flash'] = 'Стиль счётчика сохранён';
redirect('/?module=office&action=code&id=' . $id);
}
if ($do === 'toggle_hide') {
$id = (int)($_POST['id'] ?? 0);
$pf = Database::fetch('SELECT hide FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
if ($pf) {
$new = (int)$pf['hide'] ? 0 : 1;
Database::query('UPDATE platforms SET hide = ? WHERE id = ? AND user_id = ?', [$new, $id, $userId]);
$_SESSION['flash'] = $new ? 'Статистика скрыта' : 'Статистика открыта';
}
redirect('/?module=office&action=platforms');
}
if ($do === 'profile') {
$email = trim($_POST['email'] ?? '');
$pass = $_POST['password'] ?? '';
$pass2 = $_POST['password2'] ?? '';
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Некорректный email';
} elseif ($pass !== '' && mb_strlen($pass) < 6) {
$error = 'Пароль не менее 6 символов';
} elseif ($pass !== '' && $pass !== $pass2) {
$error = 'Пароли не совпадают';
} else {
Database::query('UPDATE user SET email = ? WHERE id = ?', [$email, $userId]);
if ($pass !== '') {
Database::query('UPDATE user SET password = ? WHERE id = ?', [password_hash($pass, PASSWORD_DEFAULT), $userId]);
}
$_SESSION['flash'] = 'Профиль обновлён';
redirect('/?module=office&action=profile');
}
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
function officeHeader(string $title, string $active): void
{
$tabs = [
'dashboard' => 'Обзор',
'platforms' => 'Площадки',
'add' => 'Добавить',
'ads' => 'Реклама',
'points' => 'Баллы',
'profile' => 'Профиль',
];
echo '<!DOCTYPE html><html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<?= pwa_meta() ?>
<title>' . e($title) . '</title>
<link rel="stylesheet" href="/assets/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head><body><div class="app">
<header class="topbar">
<div class="topbar-brand"><div class="logo">T</div><span>' . e($_SESSION['user_login'] ?? '') . '</span></div>
<nav class="topbar-nav">
<a href="/">Топ</a>
<a href="/?module=office&action=logout">Выход</a>
</nav>
</header>
<nav class="tabs">';
foreach ($tabs as $k => $label) {
$cls = $k === $active ? 'active' : '';
echo '<a class="' . $cls . '" href="/?module=office&action=' . $k . '">' . e($label) . '</a>';
}
echo '</nav>';
}
function officeFooter(): void
{
echo pwa_register() . '</div></body></html>';
}
// ================= CODE =================
if ($action === 'code') {
$id = (int)($_GET['id'] ?? 0);
$pf = Database::fetch('SELECT * FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
if (!$pf && $level < 9) {
redirect('/?module=office&action=platforms');
}
if (!$pf) $pf = Database::fetch('SELECT * FROM platforms WHERE id = ?', [$id]);
$codes = Counter::embedCodes($id, $base);
officeHeader('Код счётчика', 'platforms');
?>
<h1 class="page-title"><?= e($pf['url']) ?></h1>
<p class="page-sub">HTML-код счётчика</p>
<?php $styles = Counter::availableStyles(); ?>
<div class="card">
<h2>Внешний вид счётчика</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="save_styles">
<input type="hidden" name="id" value="<?= (int)$id ?>">
<label>Большой стиль</label>
<select name="style_big">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>" <?= (int)($pf['style_big'] ?? 1) === (int)$num ? 'selected' : '' ?>><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<label>Малый стиль</label>
<select name="style_small">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>" <?= (int)($pf['style_small'] ?? 1) === (int)$num ? 'selected' : '' ?>><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-primary">Сохранить стиль</button>
</form>
<div class="preview-bar" style="margin-top:14px;display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<img src="/cn/big/<?= (int)$id ?>?t=<?= time() ?>" alt="big" width="88" height="31" style="border-radius:4px;background:#111">
<img src="/cn/small/<?= (int)$id ?>?t=<?= time() ?>" alt="small" width="60" height="20" style="border-radius:4px;background:#111">
</div>
</div>
<div class="card">
<label>Большой (88×31) — HTML</label>
<textarea readonly onclick="this.select()"><?= e($codes['big']) ?></textarea>
</div>
<div class="card">
<label>Малый (60×20) — HTML</label>
<textarea readonly onclick="this.select()"><?= e($codes['small']) ?></textarea>
</div>
<div class="actions">
<a class="btn btn-ghost" href="/?module=office&action=platforms">← Площадки</a>
<a class="btn btn-primary" href="/?module=office&action=pstats&id=<?= $id ?>">Статистика</a>
</div>
<?php
officeFooter();
exit;
}
// ================= PLATFORM STATS =================
if ($action === 'pstats') {
$id = (int)($_GET['id'] ?? 0);
$pf = Database::fetch('SELECT * FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
if (!$pf) {
$_SESSION['flash'] = 'Площадка не найдена';
redirect('/?module=office&action=platforms');
}
$days = (int)($_GET['days'] ?? 14);
if (!in_array($days, [7, 14, 30], true)) {
$days = 14;
}
$hosts = Stats::dailyHosts($id, $days);
$hits = Stats::dailyHits($id, $days);
$ins = Stats::dailyIn($id, $days);
$labels = Stats::shortLabels(array_keys($hosts));
$hourly = Stats::hourlyHits($id);
$hourLabels = array_map(static function ($h) { return sprintf('%02d', $h); }, range(0, 23));
$weekday = Stats::weekdayHits($id, 28);
$weekLabels = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
$weekData = [];
for ($i = 1; $i <= 7; $i++) {
$weekData[] = (int)($weekday[$i] ?? 0);
}
$online = Stats::onlineCount($id);
$rank = Stats::rankPosition($id);
$delta = Stats::rankDelta($id);
$ctr = Stats::ctr($id);
$growth = Stats::hostGrowth($id);
$peak = Stats::peakHour($id);
$hits24 = Stats::hitsLastHours(24, $id);
$hosts24 = Stats::hostsLastHours(24, $id);
$totals = Stats::periodTotals($id, $days);
officeHeader('Статистика', 'platforms');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title"><?= e($pf['url']) ?></h1>
<p class="page-sub">
Место <?= $rank > 0 ? '#'.$rank : '—' ?>
<?php if ($delta): ?>
· <?= (int)$delta['delta'] >= 0 ? '<span style="color:var(--accent)">↑'.(int)$delta['delta'].'</span>' : '<span style="color:var(--danger)">↓'.abs((int)$delta['delta']).'</span>' ?>
<?php endif; ?>
· CTR <?= e((string)$ctr) ?>% · пик <?= sprintf('%02d', $peak['hour']) ?>:00
</p>
<div class="tabs">
<a class="<?= $days===7?'active':'' ?>" href="/?module=office&action=pstats&id=<?= $id ?>&days=7">7 дн.</a>
<a class="<?= $days===14?'active':'' ?>" href="/?module=office&action=pstats&id=<?= $id ?>&days=14">14 дн.</a>
<a class="<?= $days===30?'active':'' ?>" href="/?module=office&action=pstats&id=<?= $id ?>&days=30">30 дн.</a>
<a href="/?module=stats&id=<?= $id ?>">Публичная</a>
<a href="/?module=office&action=code&id=<?= $id ?>">Код</a>
</div>
<div class="metrics">
<div class="metric"><div class="m-label">Хосты</div><div class="m-val accent"><?= (int)$pf['hs'] ?></div></div>
<div class="metric"><div class="m-label">Хиты</div><div class="m-val"><?= (int)$pf['ht'] ?></div></div>
<div class="metric"><div class="m-label">In</div><div class="m-val gold"><?= (int)$pf['in_count'] ?></div></div>
<div class="metric"><div class="m-label">Онлайн</div><div class="m-val"><?= (int)$online ?></div></div>
<div class="metric"><div class="m-label">24ч хиты</div><div class="m-val"><?= (int)$hits24 ?></div></div>
<div class="metric"><div class="m-label">24ч хосты</div><div class="m-val"><?= (int)$hosts24 ?></div></div>
<div class="metric"><div class="m-label">Рост</div><div class="m-val"><?= ($growth['pct']>=0?'+':'').e((string)$growth['pct']) ?>%</div></div>
<div class="metric"><div class="m-label">За период</div><div class="m-val"><?= (int)($totals['hits'] ?? 0) ?></div></div>
</div>
<div class="card">
<h2>Динамика за <?= (int)$days ?> дней</h2>
<div class="chart-box" style="height:240px"><canvas id="c1"></canvas></div>
</div>
<div class="card">
<h2>Сегодня по часам</h2>
<div class="chart-box" style="height:190px"><canvas id="cHour"></canvas></div>
</div>
<div class="card">
<h2>Дни недели</h2>
<div class="chart-box" style="height:170px"><canvas id="cWeek"></canvas></div>
</div>
<div class="card">
<h2>Сводка</h2>
<p class="text-muted" style="font-size:.9rem;line-height:1.85">
Всего хостов: <strong style="color:var(--text)"><?= (int)$pf['hs_all'] ?></strong><br>
Всего хитов: <strong style="color:var(--text)"><?= (int)$pf['ht_all'] ?></strong><br>
Mobile / PC: <strong style="color:var(--text)"><?= (int)$pf['mb'] ?></strong> / <strong style="color:var(--text)"><?= (int)$pf['pc'] ?></strong><br>
Добавлена: <?= $pf['time_add'] ? date('d.m.Y H:i', (int)$pf['time_add']) : '—' ?>
</p>
</div>
<script>
const grid='rgba(255,255,255,0.06)'; Chart.defaults.color='#8b9cb8'; Chart.defaults.borderColor=grid;
const opt={responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'top',labels:{boxWidth:12}}},scales:{x:{grid:{color:grid}},y:{beginAtZero:true,grid:{color:grid}}}};
new Chart(document.getElementById('c1'),{type:'line',data:{labels:<?= json_encode($labels, JSON_UNESCAPED_UNICODE) ?>,datasets:[
{label:'Хосты',data:<?= json_encode(array_values($hosts)) ?>,borderColor:'#2ee6b8',backgroundColor:'rgba(46,230,184,0.12)',fill:true,tension:0.35,pointRadius:2},
{label:'Хиты',data:<?= json_encode(array_values($hits)) ?>,borderColor:'#6b9fff',backgroundColor:'rgba(107,159,255,0.1)',fill:true,tension:0.35,pointRadius:2},
{label:'In',data:<?= json_encode(array_values($ins)) ?>,borderColor:'#ffc94a',backgroundColor:'rgba(255,201,74,0.1)',fill:true,tension:0.35,pointRadius:2}
]},options:opt});
new Chart(document.getElementById('cHour'),{type:'bar',data:{labels:<?= json_encode($hourLabels) ?>,datasets:[{label:'Хиты',data:<?= json_encode(array_values($hourly)) ?>,backgroundColor:'rgba(107,159,255,0.7)',borderRadius:4}]},options:{...opt,plugins:{legend:{display:false}}}});
new Chart(document.getElementById('cWeek'),{type:'bar',data:{labels:<?= json_encode($weekLabels, JSON_UNESCAPED_UNICODE) ?>,datasets:[{label:'Хиты',data:<?= json_encode($weekData) ?>,backgroundColor:'rgba(46,230,184,0.65)',borderRadius:6}]},options:{...opt,plugins:{legend:{display:false}}}});
</script>
<?php
officeFooter();
exit;
}
if ($action === 'dashboard') {
$my = Database::fetchAll('SELECT * FROM platforms WHERE user_id = ? ORDER BY hs DESC', [$userId]);
$sumHs = 0; $sumHt = 0;
foreach ($my as $p) { $sumHs += (int)$p['hs']; $sumHt += (int)$p['ht']; }
$userDaily = ['hosts'=>[],'hits'=>[],'in'=>[]];
try { $userDaily = Stats::userDaily($userId, 14); } catch (Throwable $e) {}
officeHeader('Кабинет', 'dashboard');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Обзор</h1>
<p class="page-sub">Ваша статистика</p>
<div class="metrics">
<div class="metric"><div class="m-label">Площадок</div><div class="m-val"><?= count($my) ?></div></div>
<div class="metric"><div class="m-label">Хосты</div><div class="m-val accent"><?= number_format($sumHs, 0, '', ' ') ?></div></div>
<div class="metric"><div class="m-label">Хиты</div><div class="m-val"><?= number_format($sumHt, 0, '', ' ') ?></div></div>
<div class="metric"><div class="m-label">Уровень</div><div class="m-val gold"><?= $level >= 9 ? 'Admin' : 'User' ?></div></div>
</div>
<div class="card">
<h2>Сводка за 14 дней</h2>
<div class="chart-box" style="height:220px"><canvas id="dashChart"></canvas></div>
</div>
<script>
(function(){
const el=document.getElementById('dashChart'); if(!el||!window.Chart) return;
const grid='rgba(255,255,255,0.06)';
new Chart(el,{type:'line',data:{labels:<?= json_encode(Stats::shortLabels(array_keys($userDaily['hosts']??[])), JSON_UNESCAPED_UNICODE) ?>,datasets:[
{label:'Хосты',data:<?= json_encode(array_values($userDaily['hosts']??[])) ?>,borderColor:'#2ee6b8',backgroundColor:'rgba(46,230,184,0.12)',fill:true,tension:0.35,pointRadius:2},
{label:'Хиты',data:<?= json_encode(array_values($userDaily['hits']??[])) ?>,borderColor:'#6b9fff',backgroundColor:'rgba(107,159,255,0.1)',fill:true,tension:0.35,pointRadius:2},
{label:'In',data:<?= json_encode(array_values($userDaily['in']??[])) ?>,borderColor:'#ffc94a',tension:0.35,pointRadius:2}
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'top',labels:{boxWidth:12}}},scales:{x:{grid:{color:grid}},y:{beginAtZero:true,grid:{color:grid}}}}});
})();
</script>
<?php if ($my): ?>
<div class="card"><h2>Ваши площадки</h2>
<?php foreach (array_slice($my, 0, 5) as $p): ?>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);gap:8px;flex-wrap:wrap">
<div>
<strong><?= e($p['url']) ?></strong>
<?php if ((int)$p['mode']): ?><span class="badge badge-mod">MOD</span><?php endif; ?>
<?php if ((int)$p['ban']): ?><span class="badge badge-ban">BAN</span><?php endif; ?>
</div>
<div class="text-muted" style="font-size:.85rem"><?= (int)$p['hs'] ?> / <?= (int)$p['ht'] ?></div>
</div>
<?php endforeach; ?>
<a href="/?module=office&action=platforms" class="btn btn-ghost btn-sm" style="margin-top:12px">Все площадки</a>
</div>
<?php else: ?>
<div class="empty">
<div class="empty-icon">➕</div>
<div>Добавьте первую площадку</div>
<a href="/?module=office&action=add" class="btn btn-primary" style="margin-top:14px">Добавить</a>
</div>
<?php endif;
officeFooter();
exit;
}
// ================= PLATFORMS LIST =================
if ($action === 'platforms') {
$list = Database::fetchAll(
'SELECT p.*, c.name AS cat_name FROM platforms p LEFT JOIN cat c ON c.id = p.cat_id WHERE p.user_id = ? ORDER BY p.id DESC',
[$userId]
);
officeHeader('Площадки', 'platforms');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Мои площадки</h1>
<p class="page-sub"><a href="/?module=office&action=add">+ Добавить новую</a></p>
<?php if (!$list): ?>
<div class="empty"><div>Пока пусто</div></div>
<?php else: foreach ($list as $p): ?>
<article class="rank-card" style="grid-template-columns:1fr;gap:10px">
<div>
<div class="rank-url"><?= e($p['url']) ?>
<?php if ((int)$p['gold'] > time()): ?><span class="badge-gold">★ Gold</span><?php endif; ?>
<?php if ((int)$p['mode']): ?><span class="badge badge-mod">MOD</span><?php endif; ?>
<?php if ((int)$p['ban']): ?><span class="badge badge-ban">BAN</span><?php endif; ?>
</div>
<div class="rank-meta">
<span class="chip"><?= e($p['cat_name'] ?? '—') ?></span>
<span class="chip">hs <?= (int)$p['hs'] ?></span>
<span class="chip">ht <?= (int)$p['ht'] ?></span>
</div>
</div>
<div class="act-btns">
<a href="/?module=office&action=pstats&id=<?= (int)$p['id'] ?>">Статистика</a>
<a href="/?module=office&action=code&id=<?= (int)$p['id'] ?>">Код</a>
<a href="/?module=office&action=edit&id=<?= (int)$p['id'] ?>">Изменить</a>
<form method="post" style="display:inline"><?= csrf_field() ?>
<input type="hidden" name="do" value="toggle_hide">
<input type="hidden" name="id" value="<?= (int)$p['id'] ?>">
<button type="submit"><?= (int)$p['hide'] ? 'Показать стат' : 'Скрыть стат' ?></button>
</form>
<form method="post" style="display:inline" onsubmit="return confirm('Удалить площадку?')"><?= csrf_field() ?>
<input type="hidden" name="do" value="delete">
<input type="hidden" name="id" value="<?= (int)$p['id'] ?>">
<button class="danger" type="submit">Удалить</button>
</form>
</div>
</article>
<?php endforeach; endif;
officeFooter();
exit;
}
// ================= ADD =================
if ($action === 'add') {
$cats = Database::fetchAll('SELECT * FROM cat ORDER BY sort, id');
officeHeader('Добавить', 'add');
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Новая площадка</h1>
<div class="card">
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="add">
<label>Адрес (без http://)</label>
<input type="text" name="url" placeholder="example.com" required maxlength="60" inputmode="url">
<label>Описание</label>
<input type="text" name="message" maxlength="120" placeholder="Кратко о сайте">
<label>Категория</label>
<select name="cat_id">
<?php foreach ($cats as $c): ?>
<option value="<?= (int)$c['id'] ?>"><?= e($c['name']) ?></option>
<?php endforeach; ?>
</select>
<?php $styles = Counter::availableStyles(); ?>
<label>Стиль большого счётчика</label>
<select name="style_big">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>"><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<label>Стиль малого счётчика</label>
<select name="style_small">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>"><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<?php if (!empty($cfgSite['pf_mod'])): ?>
<p class="text-muted" style="font-size:.85rem;margin-bottom:12px">Новые площадки проходят модерацию</p>
<?php endif; ?>
<button type="submit" class="btn btn-primary btn-block">Добавить</button>
</form>
</div>
<?php
officeFooter();
exit;
}
// ================= EDIT =================
if ($action === 'edit') {
$id = (int)($_GET['id'] ?? 0);
$pf = Database::fetch('SELECT * FROM platforms WHERE id = ? AND user_id = ?', [$id, $userId]);
if (!$pf) redirect('/?module=office&action=platforms');
$cats = Database::fetchAll('SELECT * FROM cat ORDER BY sort, id');
officeHeader('Редактирование', 'platforms');
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Изменить площадку</h1>
<div class="card">
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="edit">
<input type="hidden" name="id" value="<?= $id ?>">
<label>URL</label>
<input type="text" name="url" value="<?= e($pf['url']) ?>" required>
<label>Описание</label>
<input type="text" name="message" value="<?= e($pf['message']) ?>" maxlength="120">
<label>Категория</label>
<select name="cat_id">
<?php foreach ($cats as $c): ?>
<option value="<?= (int)$c['id'] ?>" <?= (int)$c['id']===(int)$pf['cat_id']?'selected':'' ?>><?= e($c['name']) ?></option>
<?php endforeach; ?>
</select>
<?php $styles = Counter::availableStyles(); ?>
<label>Стиль большого счётчика</label>
<select name="style_big">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>" <?= (int)($pf['style_big'] ?? 1)===(int)$num?'selected':'' ?>><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<label>Стиль малого счётчика</label>
<select name="style_small">
<?php foreach ($styles as $num => $name): ?>
<option value="<?= (int)$num ?>" <?= (int)($pf['style_small'] ?? 1)===(int)$num?'selected':'' ?>><?= (int)$num ?>. <?= e($name) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-primary btn-block">Сохранить</button>
</form>
</div>
<?php
officeFooter();
exit;
}
// ================= PROFILE =================
if ($action === 'profile') {
$user = Database::fetch('SELECT * FROM user WHERE id = ?', [$userId]);
officeHeader('Профиль', 'profile');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Профиль</h1>
<p class="page-sub">Логин: <strong><?= e($user['login'] ?? '') ?></strong> · ID <?= $userId ?></p>
<div class="metric-row">
<div class="metric"><div class="m-label">Регистрация</div><div class="m-val blue" style="font-size:1rem"><?= !empty($user['reg_time']) ? date('d.m.Y', (int)$user['reg_time']) : '—' ?></div></div>
<div class="metric"><div class="m-label">Последний вход</div><div class="m-val blue" style="font-size:1rem"><?= !empty($user['last_time']) ? date('d.m.Y H:i', (int)$user['last_time']) : '—' ?></div></div>
</div>
<div class="card">
<h2>Настройки</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="profile">
<label>Email</label>
<input type="email" name="email" value="<?= e($user['email'] ?? '') ?>">
<label>Новый пароль (оставьте пустым, если не меняете)</label>
<input type="password" name="password" minlength="6" autocomplete="new-password">
<label>Повтор пароля</label>
<input type="password" name="password2" minlength="6" autocomplete="new-password">
<button type="submit" class="btn btn-primary btn-block">Сохранить</button>
</form>
</div>
<?php
officeFooter();
exit;
}
if ($action === 'points') {
$bal = Points::balance($userId);
$hist = Points::history($userId, 40);
$myPlats = Database::fetchAll('SELECT id, url, gold FROM platforms WHERE user_id = ? ORDER BY id DESC', [$userId]);
$inReward = (int) Points::cfg('in_reward', '1');
$goldCost = (int) Points::cfg('gold_day_cost', '50');
officeHeader('Баллы', 'points');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Баллы</h1>
<p class="page-sub">Баланс: <strong style="color:var(--accent)"><?= (int)$bal ?></strong>
· за уникальный переход +<?= (int)$inReward ?> · GOLD <?= (int)$goldCost ?> баллов/день</p>
<div class="card">
<h2>Купить GOLD за баллы</h2>
<?php if (!$myPlats): ?>
<p class="text-muted">Нет площадок</p>
<?php else: ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="buy_gold_points">
<label>Площадка</label>
<select name="platform_id" required>
<?php foreach ($myPlats as $mp): ?>
<option value="<?= (int)$mp['id'] ?>"><?= e($mp['url']) ?><?= (int)$mp['gold'] > time() ? ' (GOLD до '.date('d.m', (int)$mp['gold']).')' : '' ?></option>
<?php endforeach; ?>
</select>
<label>Дней (1–90)</label>
<input type="number" name="days" value="7" min="1" max="90" required>
<button type="submit" class="btn btn-primary btn-block">Купить GOLD</button>
</form>
<?php endif; ?>
</div>
<h2 class="page-title" style="font-size:1.05rem">История</h2>
<?php if (!$hist): ?>
<div class="empty"><div>Пока пусто</div></div>
<?php else: foreach ($hist as $h): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.85rem;display:flex;justify-content:space-between;gap:8px">
<div>
<?= e(Points::reasonLabel($h['reason'])) ?>
<span class="text-muted"><?= date('d.m.Y H:i', (int)$h['time']) ?></span>
</div>
<div style="color:<?= (int)$h['delta'] >= 0 ? 'var(--ok)' : 'var(--danger)' ?>;font-weight:700">
<?= (int)$h['delta'] >= 0 ? '+' : '' ?><?= (int)$h['delta'] ?>
<span class="text-muted" style="font-weight:500">(<?= (int)$h['balance'] ?>)</span>
</div>
</div>
<?php endforeach; endif;
officeFooter();
exit;
}
if ($action === 'ads') {
$prices = Ads::prices();
$mine = [];
try {
$mine = Database::fetchAll(
'SELECT * FROM ads WHERE user_id = ? ORDER BY id DESC LIMIT 50',
[$userId]
);
} catch (Throwable $e) {
$error = 'Реклама ещё не настроена (нужна миграция migrate_ads.sql)';
}
$positions = Ads::positions();
officeHeader('Реклама', 'ads');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($error) echo '<div class="alert alert-error">' . e($error) . '</div>';
?>
<h1 class="page-title">Реклама</h1>
<p class="page-sub">Покупка мест на сайте. Оплата вручную — после заявки свяжитесь с администрацией.</p>
<div class="card">
<h2>Заказать размещение</h2>
<?php if (!$prices): ?>
<p class="text-muted">Нет доступных мест</p>
<?php else: ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="buy_ad">
<label>Место</label>
<select name="position" id="adPos" required>
<?php foreach ($prices as $pr): ?>
<option value="<?= e($pr['position']) ?>" data-price="<?= e((string)$pr['price_day']) ?>">
<?= e($pr['title'] ?: ($positions[$pr['position']] ?? $pr['position'])) ?>
— <?= number_format((float)$pr['price_day'], 2, '.', ' ') ?> ₽/день
</option>
<?php endforeach; ?>
</select>
<label>Дней (1–90)</label>
<input type="number" name="days" id="adDays" value="7" min="1" max="90" required>
<p class="text-muted" style="font-size:.85rem">Итого примерно: <strong id="adTotal">—</strong> ₽</p>
<label>URL (без http://)</label>
<input type="text" name="url" required maxlength="60" placeholder="mysite.com">
<label>Название</label>
<input type="text" name="name" required maxlength="64">
<label>Краткий текст</label>
<input type="text" name="text" maxlength="200" placeholder="Описание">
<button type="submit" class="btn btn-primary btn-block">Отправить заявку</button>
</form>
<script>
function updAdTotal(){
var p=document.getElementById('adPos');
var d=document.getElementById('adDays');
var opt=p.options[p.selectedIndex];
var price=parseFloat(opt.getAttribute('data-price')||'0');
var days=parseInt(d.value||'0',10);
document.getElementById('adTotal').textContent=(price*days).toFixed(2);
}
document.getElementById('adPos').addEventListener('change',updAdTotal);
document.getElementById('adDays').addEventListener('input',updAdTotal);
updAdTotal();
</script>
<?php endif; ?>
</div>
<h2 class="page-title" style="font-size:1.05rem">Мои заявки</h2>
<?php if (!$mine): ?>
<div class="empty"><div>Заявок пока нет</div></div>
<?php else: foreach ($mine as $ad): ?>
<article class="rank-card" style="grid-template-columns:1fr;gap:6px">
<div>
<strong><?= e($ad['name']) ?></strong>
<span class="badge <?= (int)$ad['status']===1?'badge-ok':((int)$ad['status']===0?'badge-mod':'badge-ban') ?>">
<?= e(Ads::statusLabel((int)$ad['status'])) ?>
</span>
<div class="text-muted" style="font-size:.8rem;margin-top:4px">
<?= e($positions[$ad['position']] ?? $ad['position']) ?>
· <?= e($ad['url']) ?>
· <?= (int)$ad['days'] ?> дн.
· <?= number_format((float)$ad['price'], 2, '.', ' ') ?> ₽
<?php if ((int)$ad['time_end']): ?> · до <?= date('d.m.Y', (int)$ad['time_end']) ?><?php endif; ?>
</div>
<?php if ((int)$ad['status'] === 0): ?>
<form method="post" style="margin-top:8px">
<?= csrf_field() ?>
<input type="hidden" name="do" value="pay_ad_points">
<input type="hidden" name="ad_id" value="<?= (int)$ad['id'] ?>">
<button type="submit" class="btn btn-accent btn-sm">Оплатить баллами</button>
</form>
<?php endif; ?>
</div>
</article>
<?php endforeach; endif;
officeFooter();
exit;
}
redirect('/?module=office&action=dashboard');