Your IP : 216.73.217.78


Current Path : /home/seto/888mirror.pm/admin/
Upload File :
Current File : /home/seto/888mirror.pm/admin/dashboard.php

<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/helpers.php';

auth_check();
$db = db();

$total_brands = (int)$db->query("SELECT COUNT(*) FROM brands WHERE status='active'")->fetchColumn();
$today        = date('Y-m-d');
$month_start  = date('Y-m-01');

// === Глобальные фильтры ===
$period      = in_array($_GET['period'] ?? '', ['today','yesterday','7d','30d','all'])
               ? $_GET['period'] : '30d';
$top_exclude = ($_GET['exclude_cnus'] ?? '1') !== '0';

$excl_all = $top_exclude ? "AND country NOT IN ('cn','us')" : '';
$excl_s   = $top_exclude ? "AND s.country NOT IN ('cn','us')" : '';

// Фильтр Hetzner datacenter IPv6 (2a01:4f8:: и 2a01:4f9::) — краулеры с этих диапазонов
$hetzner_excl   = "AND (ip NOT LIKE '2a01:4f8:%' AND ip NOT LIKE '2a01:4f9:%')";
$hetzner_excl_s = "AND (s.ip NOT LIKE '2a01:4f8:%' AND s.ip NOT LIKE '2a01:4f9:%')";

$period_where = match($period) {
    'today'     => "DATE(created_at) = '" . date('Y-m-d') . "'",
    'yesterday' => "DATE(created_at) = '" . date('Y-m-d', strtotime('-1 day')) . "'",
    '7d'        => "created_at >= '" . date('Y-m-d H:i:s', strtotime('-7 days')) . "'",
    'all'       => "1=1",
    default     => "created_at >= '" . date('Y-m-d H:i:s', strtotime('-30 days')) . "'",
};
$period_where_s = match($period) {
    'today'     => "DATE(s.created_at) = '" . date('Y-m-d') . "'",
    'yesterday' => "DATE(s.created_at) = '" . date('Y-m-d', strtotime('-1 day')) . "'",
    '7d'        => "s.created_at >= '" . date('Y-m-d H:i:s', strtotime('-7 days')) . "'",
    'all'       => "1=1",
    default     => "s.created_at >= '" . date('Y-m-d H:i:s', strtotime('-30 days')) . "'",
};
$period_label = match($period) {
    'today'     => 'Сегодня',
    'yesterday' => 'Вчера',
    '7d'        => '7 дней',
    'all'       => 'Всё время',
    default     => '30 дней',
};

// Боты
$s = $db->prepare("SELECT COUNT(*) FROM access_log WHERE type='bot' AND DATE(created_at)=?");
$s->execute([$today]);
$bots_today = (int)$s->fetchColumn();
$s = $db->prepare("SELECT COUNT(*) FROM access_log WHERE type='bot' AND created_at>=?");
$s->execute([$month_start . ' 00:00:00']);
$bots_month = (int)$s->fetchColumn();

$openai_key = setting('openai_key');

// Редиректы
$s = $db->prepare("SELECT COUNT(*) FROM redirect_clicks WHERE DATE(created_at)=?");
$s->execute([$today]);
$redirects_today = (int)$s->fetchColumn();
$s = $db->prepare("SELECT COUNT(*) FROM redirect_clicks WHERE created_at>=?");
$s->execute([$month_start . ' 00:00:00']);
$redirects_month = (int)$s->fetchColumn();

// Сводка за выбранный период
$stats_period_raw = $db->query("
    SELECT event_type,
        CASE WHEN event_type='impression' THEN COUNT(DISTINCT visit_id) ELSE COUNT(*) END as cnt
    FROM stats
    WHERE $period_where AND country != '-' $excl_all $hetzner_excl
    GROUP BY event_type
")->fetchAll();
$period_data = array_column($stats_period_raw, 'cnt', 'event_type');

// Всего за всё время (правая карточка)
$stats_all_raw = $db->query("
    SELECT event_type,
        CASE WHEN event_type='impression' THEN COUNT(DISTINCT visit_id) ELSE COUNT(*) END as cnt
    FROM stats WHERE country != '-' $excl_all $hetzner_excl GROUP BY event_type
")->fetchAll();
$all_data = array_column($stats_all_raw, 'cnt', 'event_type');

// График (следует за периодом)
if (in_array($period, ['today', 'yesterday'])) {
    $chart_day = $period === 'today' ? date('Y-m-d') : date('Y-m-d', strtotime('-1 day'));
    $chart_raw = $db->query("
        SELECT CAST(strftime('%H', created_at) AS INTEGER) as slot,
            COUNT(DISTINCT CASE WHEN event_type='impression' THEN visit_id END) as imp,
            SUM(CASE WHEN event_type='click' THEN 1 ELSE 0 END) as clk
        FROM stats
        WHERE DATE(created_at) = '$chart_day' AND country != '-' $excl_all $hetzner_excl
        GROUP BY slot ORDER BY slot
    ")->fetchAll();
    $chart_by_slot = array_column($chart_raw, null, 'slot');
    $chart_labels = $chart_imp = $chart_clk = [];
    for ($h = 0; $h < 24; $h++) {
        $chart_labels[] = sprintf('%02d:00', $h);
        $chart_imp[]    = (int)($chart_by_slot[$h]['imp'] ?? 0);
        $chart_clk[]    = (int)($chart_by_slot[$h]['clk'] ?? 0);
    }
    $chart_title = 'По часам — ' . ($period === 'today' ? 'сегодня' : 'вчера');
} elseif ($period === 'all') {
    $chart_start = date('Y-m-01', strtotime('-11 months'));
    $chart_raw   = $db->query("
        SELECT strftime('%Y-%m', created_at) as slot,
            COUNT(DISTINCT CASE WHEN event_type='impression' THEN visit_id END) as imp,
            SUM(CASE WHEN event_type='click' THEN 1 ELSE 0 END) as clk
        FROM stats
        WHERE created_at >= '$chart_start' AND country != '-' $excl_all $hetzner_excl
        GROUP BY slot ORDER BY slot
    ")->fetchAll();
    $chart_by_slot = array_column($chart_raw, null, 'slot');
    $chart_labels = $chart_imp = $chart_clk = [];
    for ($i = 11; $i >= 0; $i--) {
        $m = date('Y-m', strtotime("-$i months"));
        $chart_labels[] = date('M y', strtotime($m . '-01'));
        $chart_imp[]    = (int)($chart_by_slot[$m]['imp'] ?? 0);
        $chart_clk[]    = (int)($chart_by_slot[$m]['clk'] ?? 0);
    }
    $chart_title = 'По месяцам — последние 12 мес.';
} else {
    $days      = $period === '7d' ? 7 : 30;
    $chart_raw = $db->query("
        SELECT DATE(created_at) as slot,
            COUNT(DISTINCT CASE WHEN event_type='impression' THEN visit_id END) as imp,
            SUM(CASE WHEN event_type='click' THEN 1 ELSE 0 END) as clk
        FROM stats
        WHERE created_at >= '" . date('Y-m-d', strtotime('-' . ($days - 1) . ' days')) . "' AND country != '-' $excl_all $hetzner_excl
        GROUP BY DATE(created_at) ORDER BY slot
    ")->fetchAll();
    $chart_by_slot = array_column($chart_raw, null, 'slot');
    $chart_labels = $chart_imp = $chart_clk = [];
    for ($i = $days - 1; $i >= 0; $i--) {
        $day = date('Y-m-d', strtotime("-$i days"));
        $chart_labels[] = date('d.m', strtotime($day));
        $chart_imp[]    = (int)($chart_by_slot[$day]['imp'] ?? 0);
        $chart_clk[]    = (int)($chart_by_slot[$day]['clk'] ?? 0);
    }
    $chart_title = 'По дням — ' . ($days === 7 ? 'последние 7 дней' : 'последние 30 дней');
}

// Топ брендов
$top_brands = $db->query("
    SELECT b.id, b.name, b.slug,
        COUNT(DISTINCT CASE WHEN s.event_type='impression' THEN s.visit_id END) as imp,
        SUM(CASE WHEN s.event_type='click' THEN 1 ELSE 0 END) as clk
    FROM stats s
    JOIN brands b ON b.id = s.brand_id
    WHERE $period_where_s AND s.country != '-' $excl_s $hetzner_excl_s
    GROUP BY s.brand_id
    HAVING imp > 0 OR clk > 0
    ORDER BY clk DESC
")->fetchAll();

// Language Coverage
$lang_brand           = (int)($_GET['lang_brand'] ?? 0);
$lang_brand_filter_st = $lang_brand ? "AND brand_id = $lang_brand" : '';
$lang_excl_zh         = $top_exclude ? "AND language != 'zh'" : '';
$lang_brands_list     = $db->query("SELECT id, name FROM brands WHERE status='active' ORDER BY name ASC")->fetchAll();
$lang_names_raw       = $db->query("SELECT code, name FROM languages")->fetchAll();
$lang_names           = array_column($lang_names_raw, 'name', 'code');
$lang_flag_map = [
    'en'=>'gb','zh'=>'cn','ko'=>'kr','ja'=>'jp','ar'=>'sa',
    'he'=>'il','uk'=>'ua','sv'=>'se','da'=>'dk','fi'=>'fi',
    'nb'=>'no','cs'=>'cz','sk'=>'sk','sl'=>'si','sr'=>'rs',
    'hr'=>'hr','bs'=>'ba','mk'=>'mk','sq'=>'al','bg'=>'bg',
    'ro'=>'ro','hu'=>'hu','et'=>'ee','lv'=>'lv','lt'=>'lt',
    'ka'=>'ge','hy'=>'am','az'=>'az','kk'=>'kz','uz'=>'uz',
    'tk'=>'tm','mn'=>'mn','vi'=>'vn','th'=>'th','id'=>'id',
    'ms'=>'my','tl'=>'ph','bn'=>'bd','hi'=>'in','ur'=>'pk',
    'fa'=>'ir','tr'=>'tr','el'=>'gr','pt'=>'pt','pl'=>'pl',
    'de'=>'de','fr'=>'fr','it'=>'it','es'=>'es','ru'=>'ru',
    'nl'=>'nl','be'=>'by','gl'=>'es-ga',
];
$lang_stats_raw = $db->query("
    SELECT language,
        COUNT(DISTINCT CASE WHEN event_type='impression' THEN visit_id END) as imp,
        SUM(CASE WHEN event_type='click'      THEN 1 ELSE 0 END) as clk,
        COUNT(DISTINCT CASE WHEN event_type='impression' THEN brand_id END) as brand_count
    FROM stats
    WHERE $period_where AND language != '-' AND country != '-' $lang_excl_zh $lang_brand_filter_st $hetzner_excl
    GROUP BY language
")->fetchAll();
$lang_rows = [];
foreach ($lang_stats_raw as $row) {
    $imp = (int)$row['imp']; $clk = (int)$row['clk'];
    if ($imp === 0 && $clk === 0) continue;
    $lang_rows[] = ['lc' => $row['language'], 'brands' => (int)$row['brand_count'], 'imp' => $imp, 'clk' => $clk];
}
usort($lang_rows, fn($a, $b) => $b['imp'] - $a['imp']);

// Referrer
$ref_brand        = (int)($_GET['ref_brand'] ?? 0);
$ref_brand_filter = $ref_brand ? "AND brand_id = $ref_brand" : '';
$ref_rows_raw     = $db->query("
    SELECT
        CASE WHEN referrer IS NULL OR referrer = '' THEN '__direct__' ELSE referrer END as src,
        COUNT(DISTINCT visit_id) as visits
    FROM stats
    WHERE event_type = 'impression' AND visit_id IS NOT NULL AND country != '-'
      AND $period_where $ref_brand_filter $hetzner_excl
    GROUP BY src
    ORDER BY visits DESC
")->fetchAll();

// GEO Coverage
$geo_brand           = (int)($_GET['geo_brand'] ?? 0);
$geo_excl_countries  = $top_exclude ? "'cn','us','hk'" : "'hk'";
$geo_brand_filter_bc = $geo_brand ? "AND bc.brand_id = $geo_brand" : '';
$geo_brand_filter_st = $geo_brand ? "AND brand_id = $geo_brand" : '';
$geo_brands_list     = $db->query("SELECT id, name FROM brands WHERE status='active' ORDER BY name ASC")->fetchAll();
$geo_brands_raw      = $db->query("
    SELECT bc.country_code, COUNT(DISTINCT bc.brand_id) as brand_count
    FROM brand_countries bc
    JOIN brands b ON b.id = bc.brand_id
    WHERE b.status = 'active' AND bc.country_code NOT IN ($geo_excl_countries) $geo_brand_filter_bc
    GROUP BY bc.country_code
")->fetchAll();
$geo_brand_counts = array_column($geo_brands_raw, 'brand_count', 'country_code');
$geo_stats_raw    = $db->query("
    SELECT country,
        COUNT(DISTINCT CASE WHEN event_type='impression' THEN visit_id END) as imp,
        SUM(CASE WHEN event_type='click'      THEN 1 ELSE 0 END) as clk
    FROM stats
    WHERE $period_where AND country != '-' AND country NOT IN ($geo_excl_countries) $geo_brand_filter_st $hetzner_excl
    GROUP BY country
")->fetchAll();
$geo_stats     = array_column($geo_stats_raw, null, 'country');
$geo_all_codes = array_unique(array_merge(array_keys($geo_brand_counts), array_keys($geo_stats)));
$geo_rows      = [];
foreach ($geo_all_codes as $cc) {
    $brands = (int)($geo_brand_counts[$cc] ?? 0);
    $imp    = (int)($geo_stats[$cc]['imp'] ?? 0);
    $clk    = (int)($geo_stats[$cc]['clk'] ?? 0);
    if ($brands === 0 && $imp === 0) continue;
    $geo_rows[] = compact('cc', 'brands', 'imp', 'clk');
}
usort($geo_rows, fn($a, $b) => $b['imp'] - $a['imp']);
$avg_imp    = count($geo_rows) ? array_sum(array_column($geo_rows, 'imp'))    / count($geo_rows) : 0;
$avg_brands = count($geo_rows) ? array_sum(array_column($geo_rows, 'brands')) / count($geo_rows) : 0;

function ctr(int $imp, int $clk): string {
    if (!$imp) return '0%';
    return round($clk / $imp * 100, 1) . '%';
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Дашборд — Admin</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?= admin_nav('dashboard') ?>
<div class="container-fluid px-2 px-md-4">

    <!-- Глобальный фильтр -->
    <div class="card shadow-sm mb-4">
        <div class="card-body py-2">
            <div class="d-flex align-items-center gap-3 flex-wrap">
                <span class="fw-semibold small text-muted text-nowrap">Период:</span>
                <div class="btn-group btn-group-sm" id="globalPeriodBtns">
                    <?php foreach (['today'=>'Сегодня','yesterday'=>'Вчера','7d'=>'7 дней','30d'=>'30 дней','all'=>'Всё время'] as $pk => $pl): ?>
                    <button type="button"
                            class="btn btn-outline-secondary <?= $period === $pk ? 'active' : '' ?>"
                            data-period="<?= $pk ?>"><?= $pl ?></button>
                    <?php endforeach ?>
                </div>
                <div class="vr d-none d-sm-block"></div>
                <div class="form-check mb-0">
                    <input class="form-check-input" type="checkbox" id="excludeCNUS"
                           <?= $top_exclude ? 'checked' : '' ?>>
                    <label class="form-check-label small" for="excludeCNUS">Без CN / US / ZH</label>
                </div>
            </div>
        </div>
    </div>

    <!-- Сводка -->
    <div class="row g-2 g-md-3 mb-4">
        <div class="col-6 col-md-2">
            <div class="card text-center shadow-sm h-100">
                <div class="card-body py-2 py-md-3">
                    <div class="fw-bold text-primary" style="font-size:clamp(1.1rem,4vw,2rem)"><?= $total_brands ?></div>
                    <div class="text-muted small">Активных брендов</div>
                </div>
            </div>
        </div>
        <div class="col-6 col-md-2">
            <div class="card text-center shadow-sm h-100">
                <div class="card-body py-2 py-md-3">
                    <div class="fw-bold text-success" style="font-size:clamp(1rem,3.5vw,2rem)"><?= number_format((int)($period_data['click'] ?? 0)) ?></div>
                    <div class="text-muted small">Кликов — <?= h($period_label) ?></div>
                </div>
            </div>
        </div>
        <div class="col-6 col-md-2">
            <div class="card text-center shadow-sm h-100">
                <div class="card-body py-2 py-md-3">
                    <div class="fw-bold text-info" style="font-size:clamp(1rem,3.5vw,2rem)"><?= number_format((int)($period_data['impression'] ?? 0)) ?></div>
                    <div class="text-muted small">Показов — <?= h($period_label) ?></div>
                </div>
            </div>
        </div>
        <div class="col-6 col-md-2">
            <div class="card text-center shadow-sm h-100">
                <div class="card-body py-2 py-md-3">
                    <div class="fw-bold text-warning" style="font-size:clamp(1rem,3.5vw,2rem)">
                        <?= ctr((int)($period_data['impression'] ?? 0), (int)($period_data['click'] ?? 0)) ?>
                    </div>
                    <div class="text-muted small">CTR — <?= h($period_label) ?></div>
                </div>
            </div>
        </div>
        <div class="col-12 col-md-4">
            <div class="card text-center shadow-sm border-secondary h-100">
                <div class="card-body py-2 py-md-3">
                    <div class="fw-bold text-secondary" style="font-size:clamp(1.1rem,4vw,2rem)"><?= $bots_today ?> <small class="fs-6 fw-normal">/ <?= $bots_month ?></small></div>
                    <div class="text-muted small">Ботов сегодня / мес.</div>
                </div>
            </div>
        </div>
        <div class="col-12 col-md-4">
            <div class="card text-center shadow-sm h-100" style="border-color:#10b981">
                <div class="card-body py-2 py-md-3">
                    <a href="/admin/stats.php?tab=redirects" class="text-decoration-none">
                        <div class="fw-bold" style="font-size:clamp(1.1rem,4vw,2rem);color:#10b981"><?= $redirects_today ?> <small class="fs-6 fw-normal text-muted">/ <?= $redirects_month ?></small></div>
                        <div class="text-muted small">↪ Redirects сегодня / мес.</div>
                    </a>
                </div>
            </div>
        </div>
    </div>

    <div class="row g-4">
        <!-- Топ брендов -->
        <div class="col-md-7">
            <div class="card shadow-sm">
                <div class="card-header d-flex align-items-center justify-content-between">
                    <span class="fw-semibold">Топ брендов</span>
                    <small class="text-muted"><?= h($period_label) ?><?= $top_exclude ? ' · без CN/US' : '' ?></small>
                </div>
                <div class="card-body p-0">
                    <div class="table-responsive">
                    <table class="table table-sm mb-0" id="topBrandsTable">
                        <thead class="table-light">
                            <tr>
                                <th style="width:32px">#</th>
                                <th>Бренд</th>
                                <th class="text-end sortable" data-col="imp" style="cursor:pointer" title="Сортировать">
                                    Показы <span class="sort-icon text-muted">↕</span>
                                </th>
                                <th class="text-end sortable" data-col="clk" style="cursor:pointer" title="Сортировать">
                                    Клики <span class="sort-icon text-muted">↓</span>
                                </th>
                                <th class="text-end sortable" data-col="ctr" style="cursor:pointer" title="Сортировать">
                                    CTR <span class="sort-icon text-muted">↕</span>
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                        <?php foreach ($top_brands as $i => $b): ?>
                            <tr>
                                <td class="text-muted"><?= $i+1 ?></td>
                                <td>
                                    <?php
                                    $geo_link = '?' . http_build_query([
                                        'period'       => $period,
                                        'exclude_cnus' => $top_exclude ? '1' : '0',
                                        'geo_brand'    => $b['id'],
                                    ]) . '#geo-coverage';
                                    ?>
                                    <a href="<?= h($geo_link) ?>" class="text-decoration-none text-body">
                                        <?= h($b['name']) ?>
                                    </a>
                                </td>
                                <td class="text-end" data-imp="<?= (int)$b['imp'] ?>"><?= number_format((int)$b['imp']) ?></td>
                                <td class="text-end" data-clk="<?= (int)$b['clk'] ?>"><?= number_format((int)$b['clk']) ?></td>
                                <?php $ctr_val = $b['imp'] > 0 ? round($b['clk'] / $b['imp'] * 100, 1) : 0; ?>
                                <td class="text-end" data-ctr="<?= $ctr_val ?>"><?= $ctr_val ?>%</td>
                            </tr>
                        <?php endforeach ?>
                        <?php if (empty($top_brands)): ?>
                            <tr><td colspan="5" class="text-center text-muted py-3">Нет данных</td></tr>
                        <?php endif ?>
                        </tbody>
                    </table>
                    </div>
                </div>
            </div>
        </div>

        <!-- Правая колонка -->
        <div class="col-md-5">
            <?php if (!$openai_key): ?>
                <div class="alert alert-warning">
                    ⚠ OpenAI API ключ не задан.
                    <a href="/admin/settings.php" class="alert-link">Добавить в настройках →</a>
                </div>
            <?php endif ?>

<!-- World Map -->
            <div class="card shadow-sm mb-3" id="world-map-card">
                <div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2"
                     style="cursor:pointer;user-select:none" onclick="toggleMap(event)">
                    <span class="fw-semibold">🌍 География</span>
                    <div class="d-flex align-items-center gap-2">
                        <div class="btn-group btn-group-sm">
                            <button class="btn btn-outline-secondary active" id="map-mode-imp"
                                    onclick="event.stopPropagation();setMapMode('imp')">Показы</button>
                            <button class="btn btn-outline-secondary" id="map-mode-clk"
                                    onclick="event.stopPropagation();setMapMode('clk')">Клики</button>
                        </div>
                        <span class="badge bg-success" id="map-live" style="font-size:.68rem;letter-spacing:.3px">● LIVE</span>
                        <i class="bi bi-chevron-down" id="map-chevron" style="transition:transform .3s;font-size:.9rem"></i>
                    </div>
                </div>
                <div style="background:#eef1f7;border-radius:0 0 .375rem .375rem;overflow:hidden">
                    <div id="world-map-mini" style="width:100%;height:460px;overflow:hidden;cursor:pointer" onclick="toggleMap(event)"></div>
                    <div id="world-map-full" style="display:none">
                        <div id="world-map" style="width:100%;height:520px;position:relative;overflow:hidden"></div>
                    </div>
                </div>
            </div>

            <!-- Всего за всё время -->
            <div class="card shadow-sm">
                <div class="card-header d-flex align-items-center justify-content-between">
                    <span>Всего за всё время</span>
                    <?php if ($top_exclude): ?>
                        <small class="text-muted fw-normal">без CN/US</small>
                    <?php endif ?>
                </div>
                <div class="card-body">
                    <table class="table table-sm mb-0">
                        <tr><td>Показы</td><td class="fw-bold"><?= number_format((int)($all_data['impression'] ?? 0)) ?></td></tr>
                        <tr><td>Клики</td><td class="fw-bold"><?= number_format((int)($all_data['click'] ?? 0)) ?></td></tr>
                        <tr><td>CTR</td><td class="fw-bold"><?= ctr((int)($all_data['impression']??0),(int)($all_data['click']??0)) ?></td></tr>
                    </table>
                </div>
            </div>
        </div>
    </div>

    <!-- Drill-down modal -->
    <div class="modal fade" id="mapDrillModal" tabindex="-1">
        <div class="modal-dialog modal-lg modal-dialog-centered">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="mapDrillTitle">—</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body" id="mapDrillBody">
                    <div class="text-center py-4 text-muted">Загрузка...</div>
                </div>
            </div>
        </div>
    </div>

    <!-- График трафика -->
    <div class="card shadow-sm mt-4">
        <div class="card-header d-flex align-items-center justify-content-between">
            <span class="fw-semibold"><?= h($chart_title) ?></span>
            <?php if ($top_exclude): ?>
                <small class="text-muted">без CN/US</small>
            <?php endif ?>
        </div>
        <div class="card-body">
            <div style="position:relative;height:220px">
                <canvas id="trafficChart"></canvas>
            </div>
        </div>
    </div>

    <!-- GEO Coverage -->
    <div id="geo-coverage" class="card shadow-sm mt-4 mb-4">
        <div class="card-header">
            <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
                <span class="fw-semibold">GEO Coverage<?php if ($top_exclude): ?> <small class="text-muted fw-normal">без CN/US</small><?php endif ?></span>
                <select id="geoBrandFilter" class="form-select form-select-sm" style="max-width:180px">
                    <option value="0">Все бренды</option>
                    <?php foreach ($geo_brands_list as $bl): ?>
                        <option value="<?= $bl['id'] ?>" <?= $geo_brand === (int)$bl['id'] ? 'selected' : '' ?>>
                            <?= h($bl['name']) ?>
                        </option>
                    <?php endforeach ?>
                </select>
            </div>
        </div>
        <div class="card-body p-0">
            <?php if (empty($geo_rows)): ?>
                <p class="text-muted text-center py-3 mb-0">Нет данных</p>
            <?php else: ?>
            <div class="table-responsive">
                <table class="table table-sm table-hover mb-0" id="geoCoverageTable">
                    <thead class="table-light">
                        <tr>
                            <th style="width:40px">#</th>
                            <th class="geo-sortable" data-col="geo" style="cursor:pointer">GEO <span class="geo-sort-icon text-muted">↕</span></th>
                            <th class="text-center geo-sortable" data-col="brands" style="cursor:pointer">Брендов <span class="geo-sort-icon text-muted">↕</span></th>
                            <th class="text-center geo-sortable" data-col="imp" style="cursor:pointer">Показы <span class="geo-sort-icon text-muted">↓</span></th>
                            <th class="text-center geo-sortable" data-col="clk" style="cursor:pointer">Клики <span class="geo-sort-icon text-muted">↕</span></th>
                            <th class="text-center geo-sortable" data-col="ctr" style="cursor:pointer">CTR <span class="geo-sort-icon text-muted">↕</span></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($geo_rows as $i => $row):
                        $is_problem   = $row['imp'] > $avg_imp && $row['brands'] < $avg_brands;
                        $row_class    = $is_problem ? 'table-danger' : '';
                        $flag_icon    = $row['cc'] === 'crypto' ? '💎' : ($row['cc'] === 'en' ? '🌐' : ($row['cc'] === '-' ? '❓' : null));
                        $landing_url  = $row['cc'] !== '-' ? '/?country=' . urlencode($row['cc']) : null;
                        $ctr_val      = $row['imp'] > 0 ? round($row['clk'] / $row['imp'] * 100, 1) : 0;
                        $country_title = match($row['cc']) {
                            'en'     => 'Global',
                            'crypto' => 'Crypto',
                            '-'      => 'Unknown / Not set',
                            default  => country_name($row['cc']),
                        };
                    ?>
                        <tr class="<?= $row_class ?>"<?= $landing_url ? ' style="cursor:pointer" onclick="window.open(\'' . h($landing_url) . '\',\'_blank\')"' : '' ?>>
                            <td class="text-muted"><?= $i + 1 ?></td>
                            <td data-geo="<?= h($row['cc']) ?>" title="<?= h($country_title) ?>">
                                <div class="d-flex align-items-center gap-2">
                                    <?php if ($flag_icon): ?>
                                        <span><?= $flag_icon ?></span>
                                    <?php else: ?>
                                        <img src="https://flagicons.lipis.dev/flags/4x3/<?= h($row['cc']) ?>.svg"
                                             width="20" height="15" style="border-radius:2px;flex-shrink:0"
                                             alt="<?= h($country_title) ?>" title="<?= h($country_title) ?>">
                                    <?php endif ?>
                                    <span class="fw-semibold"><?= $row['cc'] === '-' ? '—' : h(strtoupper($row['cc'])) ?></span>
                                    <?php if ($is_problem): ?>
                                        <span class="badge bg-danger" title="Популярное GEO, мало брендов">!</span>
                                    <?php endif ?>
                                </div>
                            </td>
                            <td class="text-center" data-brands="<?= $row['brands'] ?>">
                                <?php if ($row['brands'] === 0): ?>
                                    <span class="text-danger fw-bold">0</span>
                                <?php else: ?>
                                    <a href="/admin/brands.php" class="text-decoration-none"><?= $row['brands'] ?></a>
                                <?php endif ?>
                            </td>
                            <td class="text-center" data-imp="<?= $row['imp'] ?>"><?= number_format($row['imp']) ?></td>
                            <td class="text-center" data-clk="<?= $row['clk'] ?>"><?= number_format($row['clk']) ?></td>
                            <td class="text-center" data-ctr="<?= $ctr_val ?>"><?= $ctr_val ?>%</td>
                        </tr>
                    <?php endforeach ?>
                    </tbody>
                </table>
            </div>
            <?php endif ?>
        </div>
        <?php if (!empty($geo_rows)): ?>
        <div class="card-footer text-muted small">
            <span class="badge bg-danger me-1">!</span> — популярное GEO с малым кол-вом брендов (выше среднего по трафику, ниже среднего по брендам)
        </div>
        <?php endif ?>
    </div>

    <!-- Language Coverage -->
    <div id="lang-coverage" class="card shadow-sm mb-4">
        <div class="card-header">
            <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
                <span class="fw-semibold">Language Coverage</span>
                <select id="langBrandFilter" class="form-select form-select-sm" style="max-width:180px">
                    <option value="0">Все бренды</option>
                    <?php foreach ($lang_brands_list as $bl): ?>
                        <option value="<?= $bl['id'] ?>" <?= $lang_brand === (int)$bl['id'] ? 'selected' : '' ?>>
                            <?= h($bl['name']) ?>
                        </option>
                    <?php endforeach ?>
                </select>
            </div>
        </div>
        <div class="card-body p-0">
            <?php if (empty($lang_rows)): ?>
                <p class="text-muted text-center py-3 mb-0">Нет данных</p>
            <?php else: ?>
            <div class="table-responsive">
                <table class="table table-sm table-hover mb-0" id="langCoverageTable">
                    <thead class="table-light">
                        <tr>
                            <th style="width:40px">#</th>
                            <th class="lang-sortable" data-col="lang" style="cursor:pointer">Язык <span class="lang-sort-icon text-muted">↕</span></th>
                            <th class="text-center lang-sortable" data-col="brands" style="cursor:pointer">Брендов <span class="lang-sort-icon text-muted">↕</span></th>
                            <th class="text-center lang-sortable" data-col="imp" style="cursor:pointer">Показы <span class="lang-sort-icon text-muted">↓</span></th>
                            <th class="text-center lang-sortable" data-col="clk" style="cursor:pointer">Клики <span class="lang-sort-icon text-muted">↕</span></th>
                            <th class="text-center lang-sortable" data-col="ctr" style="cursor:pointer">CTR <span class="lang-sort-icon text-muted">↕</span></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($lang_rows as $i => $row):
                        $ctr_val   = $row['imp'] > 0 ? round($row['clk'] / $row['imp'] * 100, 1) : 0;
                        $flag_cc   = $lang_flag_map[$row['lc']] ?? $row['lc'];
                        $lang_name = $lang_names[$row['lc']] ?? strtoupper($row['lc']);
                    ?>
                        <tr>
                            <td class="text-muted"><?= $i + 1 ?></td>
                            <td data-lang="<?= h($row['lc']) ?>" title="<?= h($lang_name) ?>">
                                <div class="d-flex align-items-center gap-2">
                                    <img src="https://flagicons.lipis.dev/flags/4x3/<?= h($flag_cc) ?>.svg"
                                         width="20" height="15" style="border-radius:2px;flex-shrink:0"
                                         alt="<?= h($lang_name) ?>" title="<?= h($lang_name) ?>">
                                    <span class="fw-semibold"><?= h(strtoupper($row['lc'])) ?></span>
                                    <span class="text-muted small"><?= h($lang_name) ?></span>
                                </div>
                            </td>
                            <td class="text-center" data-brands="<?= $row['brands'] ?>">
                                <?php if ($row['brands'] === 0): ?>
                                    <span class="text-danger fw-bold">0</span>
                                <?php else: ?>
                                    <a href="/admin/translations.php" class="text-decoration-none"><?= $row['brands'] ?></a>
                                <?php endif ?>
                            </td>
                            <td class="text-center" data-imp="<?= $row['imp'] ?>"><?= number_format($row['imp']) ?></td>
                            <td class="text-center" data-clk="<?= $row['clk'] ?>"><?= number_format($row['clk']) ?></td>
                            <td class="text-center" data-ctr="<?= $ctr_val ?>"><?= $ctr_val ?>%</td>
                        </tr>
                    <?php endforeach ?>
                    </tbody>
                </table>
            </div>
            <?php endif ?>
        </div>
    </div>

    <!-- Источники трафика -->
    <div id="ref-coverage" class="card shadow-sm mb-4">
        <div class="card-header">
            <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
                <span class="fw-semibold">Источники трафика</span>
                <select id="refBrandFilter" class="form-select form-select-sm" style="max-width:180px">
                    <option value="0">Все бренды</option>
                    <?php foreach ($geo_brands_list as $bl): ?>
                        <option value="<?= $bl['id'] ?>" <?= $ref_brand === (int)$bl['id'] ? 'selected' : '' ?>>
                            <?= h($bl['name']) ?>
                        </option>
                    <?php endforeach ?>
                </select>
            </div>
        </div>
        <div class="card-body p-0">
            <?php if (empty($ref_rows_raw)): ?>
                <p class="text-muted text-center py-3 mb-0">Нет данных</p>
            <?php else: ?>
            <div class="table-responsive">
                <table class="table table-sm table-hover mb-0" id="refTable">
                    <thead class="table-light">
                        <tr>
                            <th style="width:40px">#</th>
                            <th class="ref-sortable" data-col="src" style="cursor:pointer">Источник <span class="ref-sort-icon text-muted">↕</span></th>
                            <th class="text-end ref-sortable" data-col="visits" style="cursor:pointer">Визитов <span class="ref-sort-icon text-muted">↓</span></th>
                            <th class="text-end" style="width:120px">Доля</th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php
                    $ref_total = array_sum(array_column($ref_rows_raw, 'visits'));
                    foreach ($ref_rows_raw as $i => $rr):
                        $is_direct = $rr['src'] === '__direct__';
                        $share = $ref_total > 0 ? round($rr['visits'] / $ref_total * 100, 1) : 0;
                    ?>
                        <tr>
                            <td class="text-muted"><?= $i + 1 ?></td>
                            <td data-src="<?= $is_direct ? '' : h($rr['src']) ?>">
                                <?php if ($is_direct): ?>
                                    <span class="text-muted fst-italic">Direct / None</span>
                                <?php else: ?>
                                    <a href="https://<?= h($rr['src']) ?>" target="_blank" rel="noopener"
                                       class="text-decoration-none">
                                        <img src="https://www.google.com/s2/favicons?domain=<?= urlencode($rr['src']) ?>&sz=16"
                                             width="16" height="16" class="me-1" style="vertical-align:middle;border-radius:2px"
                                             onerror="this.style.display='none'">
                                        <?= h($rr['src']) ?>
                                    </a>
                                <?php endif ?>
                            </td>
                            <td class="text-end" data-visits="<?= $rr['visits'] ?>"><?= number_format($rr['visits']) ?></td>
                            <td class="text-end">
                                <div class="d-flex align-items-center justify-content-end gap-2">
                                    <div class="progress flex-grow-1" style="height:6px;max-width:60px">
                                        <div class="progress-bar bg-primary" style="width:<?= $share ?>%"></div>
                                    </div>
                                    <span class="text-muted small" style="min-width:36px"><?= $share ?>%</span>
                                </div>
                            </td>
                        </tr>
                    <?php endforeach ?>
                    </tbody>
                </table>
            </div>
            <?php endif ?>
        </div>
    </div>

</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script>
(function () {
    var labels  = <?= json_encode($chart_labels) ?>;
    var impData = <?= json_encode($chart_imp) ?>;
    var clkData = <?= json_encode($chart_clk) ?>;

    var ctx = document.getElementById('trafficChart').getContext('2d');
    new Chart(ctx, {
        data: {
            labels: labels,
            datasets: [
                {
                    type: 'bar',
                    label: 'Показы',
                    data: impData,
                    backgroundColor: 'rgba(13, 110, 253, 0.25)',
                    borderColor: 'rgba(13, 110, 253, 0.6)',
                    borderWidth: 1,
                    borderRadius: 3,
                    yAxisID: 'yImp',
                    order: 2
                },
                {
                    type: 'line',
                    label: 'Клики',
                    data: clkData,
                    borderColor: '#fd7e14',
                    backgroundColor: 'rgba(253, 126, 20, 0.12)',
                    borderWidth: 2,
                    pointRadius: 3,
                    pointHoverRadius: 5,
                    pointBackgroundColor: '#fd7e14',
                    tension: 0.3,
                    fill: true,
                    yAxisID: 'yClk',
                    order: 1
                }
            ]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            interaction: { mode: 'index', intersect: false },
            plugins: {
                legend: { position: 'top', labels: { boxWidth: 12, padding: 16 } },
                tooltip: {
                    callbacks: {
                        label: function (ctx) {
                            return ' ' + ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString();
                        }
                    }
                }
            },
            scales: {
                x: {
                    grid: { display: false },
                    ticks: { maxRotation: 45, font: { size: 11 } }
                },
                yImp: {
                    type: 'linear', position: 'left', beginAtZero: true,
                    grid: { color: 'rgba(0,0,0,0.05)' },
                    ticks: { font: { size: 11 } },
                    title: { display: true, text: 'Показы', font: { size: 11 } }
                },
                yClk: {
                    type: 'linear', position: 'right', beginAtZero: true,
                    grid: { drawOnChartArea: false },
                    ticks: { font: { size: 11 } },
                    title: { display: true, text: 'Клики', font: { size: 11 } }
                }
            }
        }
    });
})();
</script>
<script>
(function () {
    function getParams() { return new URLSearchParams(window.location.search); }
    function navigate(overrides, anchor) {
        var p = getParams();
        Object.keys(overrides).forEach(function (k) { p.set(k, overrides[k]); });
        window.location.href = '?' + p.toString() + (anchor ? '#' + anchor : '');
    }

    // Глобальный период
    document.querySelectorAll('#globalPeriodBtns button').forEach(function (btn) {
        btn.addEventListener('click', function () { navigate({ period: btn.dataset.period }); });
    });

    // Exclude CN/US
    document.getElementById('excludeCNUS').addEventListener('change', function () {
        navigate({ exclude_cnus: this.checked ? '1' : '0' });
    });

    // Бренд-фильтры
    document.getElementById('geoBrandFilter').addEventListener('change', function () {
        navigate({ geo_brand: this.value }, 'geo-coverage');
    });
    document.getElementById('langBrandFilter')?.addEventListener('change', function () {
        navigate({ lang_brand: this.value }, 'lang-coverage');
    });
    document.getElementById('refBrandFilter')?.addEventListener('change', function () {
        navigate({ ref_brand: this.value }, 'ref-coverage');
    });

    // ---- Сортировка Топ брендов ----
    var sortCol = 'clk', sortAsc = false;

    function updateIcons() {
        document.querySelectorAll('#topBrandsTable th.sortable').forEach(function (th) {
            var icon = th.querySelector('.sort-icon');
            if (th.dataset.col === sortCol) {
                icon.textContent = sortAsc ? '↑' : '↓';
                icon.classList.remove('text-muted');
            } else {
                icon.textContent = '↕';
                icon.classList.add('text-muted');
            }
        });
    }

    function sortTable() {
        var tbody = document.querySelector('#topBrandsTable tbody');
        var rows  = Array.from(tbody.querySelectorAll('tr'));
        rows.sort(function (a, b) {
            var av = parseFloat(a.querySelector('[data-' + sortCol + ']')?.dataset[sortCol] ?? 0);
            var bv = parseFloat(b.querySelector('[data-' + sortCol + ']')?.dataset[sortCol] ?? 0);
            return sortAsc ? av - bv : bv - av;
        });
        rows.forEach(function (r, i) {
            var numCell = r.querySelector('td:first-child');
            if (numCell) numCell.textContent = i + 1;
            tbody.appendChild(r);
        });
        updateIcons();
    }

    document.querySelectorAll('#topBrandsTable th.sortable').forEach(function (th) {
        th.addEventListener('click', function () {
            if (sortCol === th.dataset.col) { sortAsc = !sortAsc; }
            else { sortCol = th.dataset.col; sortAsc = false; }
            sortTable();
        });
    });
    updateIcons();

    // ---- GEO Coverage: сортировка + collapse ----
    var GEO_LIMIT = 25, geoExpanded = false, geoSortCol = 'imp', geoSortAsc = false;

    function updateGeoIcons() {
        document.querySelectorAll('#geoCoverageTable th.geo-sortable').forEach(function (th) {
            var icon = th.querySelector('.geo-sort-icon');
            if (th.dataset.col === geoSortCol) {
                icon.textContent = geoSortAsc ? '↑' : '↓'; icon.classList.remove('text-muted');
            } else { icon.textContent = '↕'; icon.classList.add('text-muted'); }
        });
    }

    function applyGeoCollapse() {
        var tbody = document.querySelector('#geoCoverageTable tbody');
        if (!tbody) return;
        var rows = Array.from(tbody.querySelectorAll('tr.geo-data-row'));
        var old  = tbody.querySelector('tr.geo-toggle-row');
        if (old) old.remove();
        if (rows.length <= GEO_LIMIT) return;
        var hidden = rows.length - GEO_LIMIT;
        rows.forEach(function (r, i) { r.style.display = (geoExpanded || i < GEO_LIMIT) ? '' : 'none'; });
        var tr = document.createElement('tr');
        tr.className = 'geo-toggle-row';
        tr.innerHTML = '<td colspan="6" class="text-center py-2"><button class="btn btn-sm btn-outline-secondary px-4" id="geoToggleBtn">'
            + (geoExpanded ? '↑ Свернуть' : '+ показать ещё ' + hidden + ' стран') + '</button></td>';
        if (geoExpanded) tbody.appendChild(tr);
        else rows[GEO_LIMIT - 1].after(tr);
        document.getElementById('geoToggleBtn').addEventListener('click', function () {
            geoExpanded = !geoExpanded; applyGeoCollapse();
        });
    }

    function sortGeoTable() {
        var tbody = document.querySelector('#geoCoverageTable tbody');
        if (!tbody) return;
        var old = tbody.querySelector('tr.geo-toggle-row'); if (old) old.remove();
        var rows = Array.from(tbody.querySelectorAll('tr.geo-data-row'));
        rows.sort(function (a, b) {
            if (geoSortCol === 'geo') {
                var av = (a.querySelector('[data-geo]')?.dataset.geo || '').toLowerCase();
                var bv = (b.querySelector('[data-geo]')?.dataset.geo || '').toLowerCase();
                return geoSortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
            }
            var av = parseFloat(a.querySelector('[data-' + geoSortCol + ']')?.dataset[geoSortCol] ?? 0);
            var bv = parseFloat(b.querySelector('[data-' + geoSortCol + ']')?.dataset[geoSortCol] ?? 0);
            return geoSortAsc ? av - bv : bv - av;
        });
        rows.forEach(function (r, i) {
            var n = r.querySelector('td:first-child'); if (n) n.textContent = i + 1;
            tbody.appendChild(r);
        });
        updateGeoIcons(); applyGeoCollapse();
    }

    document.querySelectorAll('#geoCoverageTable th.geo-sortable').forEach(function (th) {
        th.addEventListener('click', function () {
            if (geoSortCol === th.dataset.col) { geoSortAsc = !geoSortAsc; }
            else { geoSortCol = th.dataset.col; geoSortAsc = false; }
            sortGeoTable();
        });
    });
    document.querySelectorAll('#geoCoverageTable tbody tr').forEach(function (r) { r.classList.add('geo-data-row'); });
    updateGeoIcons(); applyGeoCollapse();

    // ---- Language Coverage: сортировка + collapse ----
    var LANG_LIMIT = 25, langExpanded = false, langSortCol = 'imp', langSortAsc = false;

    function updateLangIcons() {
        document.querySelectorAll('#langCoverageTable th.lang-sortable').forEach(function (th) {
            var icon = th.querySelector('.lang-sort-icon');
            if (th.dataset.col === langSortCol) {
                icon.textContent = langSortAsc ? '↑' : '↓'; icon.classList.remove('text-muted');
            } else { icon.textContent = '↕'; icon.classList.add('text-muted'); }
        });
    }

    function applyLangCollapse() {
        var tbody = document.querySelector('#langCoverageTable tbody');
        if (!tbody) return;
        var rows = Array.from(tbody.querySelectorAll('tr.lang-data-row'));
        var old  = tbody.querySelector('tr.lang-toggle-row'); if (old) old.remove();
        if (rows.length <= LANG_LIMIT) return;
        var hidden = rows.length - LANG_LIMIT;
        rows.forEach(function (r, i) { r.style.display = (langExpanded || i < LANG_LIMIT) ? '' : 'none'; });
        var tr = document.createElement('tr');
        tr.className = 'lang-toggle-row';
        tr.innerHTML = '<td colspan="6" class="text-center py-2"><button class="btn btn-sm btn-outline-secondary px-4" id="langToggleBtn">'
            + (langExpanded ? '↑ Свернуть' : '+ показать ещё ' + hidden + ' языков') + '</button></td>';
        if (langExpanded) tbody.appendChild(tr);
        else rows[LANG_LIMIT - 1].after(tr);
        document.getElementById('langToggleBtn').addEventListener('click', function () {
            langExpanded = !langExpanded; applyLangCollapse();
        });
    }

    function sortLangTable() {
        var tbody = document.querySelector('#langCoverageTable tbody');
        if (!tbody) return;
        var old = tbody.querySelector('tr.lang-toggle-row'); if (old) old.remove();
        var rows = Array.from(tbody.querySelectorAll('tr.lang-data-row'));
        rows.sort(function (a, b) {
            if (langSortCol === 'lang') {
                var av = (a.querySelector('[data-lang]')?.dataset.lang || '').toLowerCase();
                var bv = (b.querySelector('[data-lang]')?.dataset.lang || '').toLowerCase();
                return langSortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
            }
            var av = parseFloat(a.querySelector('[data-' + langSortCol + ']')?.dataset[langSortCol] ?? 0);
            var bv = parseFloat(b.querySelector('[data-' + langSortCol + ']')?.dataset[langSortCol] ?? 0);
            return langSortAsc ? av - bv : bv - av;
        });
        rows.forEach(function (r, i) {
            var n = r.querySelector('td:first-child'); if (n) n.textContent = i + 1;
            tbody.appendChild(r);
        });
        updateLangIcons(); applyLangCollapse();
    }

    document.querySelectorAll('#langCoverageTable th.lang-sortable').forEach(function (th) {
        th.addEventListener('click', function () {
            if (langSortCol === th.dataset.col) { langSortAsc = !langSortAsc; }
            else { langSortCol = th.dataset.col; langSortAsc = false; }
            sortLangTable();
        });
    });
    document.querySelectorAll('#langCoverageTable tbody tr').forEach(function (r) { r.classList.add('lang-data-row'); });
    updateLangIcons(); applyLangCollapse();

    // ---- Referrer: сортировка + collapse ----
    var REF_LIMIT = 25, refExpanded = false, refSortCol = 'visits', refSortAsc = false;

    function updateRefIcons() {
        document.querySelectorAll('#refTable th.ref-sortable').forEach(function (th) {
            var icon = th.querySelector('.ref-sort-icon');
            if (th.dataset.col === refSortCol) {
                icon.textContent = refSortAsc ? '↑' : '↓'; icon.classList.remove('text-muted');
            } else { icon.textContent = '↕'; icon.classList.add('text-muted'); }
        });
    }

    function applyRefCollapse() {
        var tbody = document.querySelector('#refTable tbody');
        if (!tbody) return;
        var rows = Array.from(tbody.querySelectorAll('tr.ref-data-row'));
        var old  = tbody.querySelector('tr.ref-toggle-row'); if (old) old.remove();
        if (rows.length <= REF_LIMIT) return;
        var hidden = rows.length - REF_LIMIT;
        rows.forEach(function (r, i) { r.style.display = (refExpanded || i < REF_LIMIT) ? '' : 'none'; });
        var tr = document.createElement('tr');
        tr.className = 'ref-toggle-row';
        tr.innerHTML = '<td colspan="4" class="text-center py-2"><button class="btn btn-sm btn-outline-secondary px-4" id="refToggleBtn">'
            + (refExpanded ? '↑ Свернуть' : '+ показать ещё ' + hidden + ' источников') + '</button></td>';
        if (refExpanded) tbody.appendChild(tr);
        else rows[REF_LIMIT - 1].after(tr);
        document.getElementById('refToggleBtn').addEventListener('click', function () {
            refExpanded = !refExpanded; applyRefCollapse();
        });
    }

    function sortRefTable() {
        var tbody = document.querySelector('#refTable tbody');
        if (!tbody) return;
        var old = tbody.querySelector('tr.ref-toggle-row'); if (old) old.remove();
        var rows = Array.from(tbody.querySelectorAll('tr.ref-data-row'));
        rows.sort(function (a, b) {
            if (refSortCol === 'src') {
                var av = (a.querySelector('[data-src]')?.dataset.src || '').toLowerCase();
                var bv = (b.querySelector('[data-src]')?.dataset.src || '').toLowerCase();
                return refSortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
            }
            var av = parseFloat(a.querySelector('[data-visits]')?.dataset.visits ?? 0);
            var bv = parseFloat(b.querySelector('[data-visits]')?.dataset.visits ?? 0);
            return refSortAsc ? av - bv : bv - av;
        });
        rows.forEach(function (r, i) {
            var n = r.querySelector('td:first-child'); if (n) n.textContent = i + 1;
            tbody.appendChild(r);
        });
        updateRefIcons(); applyRefCollapse();
    }

    document.querySelectorAll('#refTable th.ref-sortable').forEach(function (th) {
        th.addEventListener('click', function () {
            if (refSortCol === th.dataset.col) { refSortAsc = !refSortAsc; }
            else { refSortCol = th.dataset.col; refSortAsc = false; }
            sortRefTable();
        });
    });
    document.querySelectorAll('#refTable tbody tr').forEach(function (r) { r.classList.add('ref-data-row'); });
    updateRefIcons(); applyRefCollapse();

})();
</script>

<!-- World Map: D3 + TopoJSON -->
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
<script>
(function(){
// ISO numeric → alpha-2
var N2A = {4:'af',8:'al',12:'dz',24:'ao',32:'ar',36:'au',40:'at',31:'az',
50:'bd',112:'by',56:'be',64:'bt',68:'bo',70:'ba',76:'br',100:'bg',116:'kh',
120:'cm',124:'ca',152:'cl',156:'cn',170:'co',188:'cr',191:'hr',192:'cu',
196:'cy',203:'cz',208:'dk',218:'ec',818:'eg',222:'sv',233:'ee',231:'et',
246:'fi',250:'fr',268:'ge',276:'de',288:'gh',300:'gr',320:'gt',332:'ht',
340:'hn',348:'hu',356:'in',360:'id',364:'ir',368:'iq',372:'ie',376:'il',
380:'it',392:'jp',400:'jo',398:'kz',404:'ke',410:'kr',414:'kw',417:'kg',
418:'la',422:'lb',428:'lv',434:'ly',440:'lt',442:'lu',807:'mk',458:'my',
484:'mx',496:'mn',504:'ma',508:'mz',524:'np',528:'nl',554:'nz',558:'ni',
566:'ng',578:'no',512:'om',586:'pk',591:'pa',600:'py',604:'pe',608:'ph',
616:'pl',620:'pt',634:'qa',642:'ro',643:'ru',682:'sa',686:'sn',710:'za',
724:'es',144:'lk',729:'sd',752:'se',756:'ch',760:'sy',762:'tj',834:'tz',
764:'th',792:'tr',795:'tm',800:'ug',804:'ua',784:'ae',826:'gb',840:'us',
858:'uy',860:'uz',862:'ve',704:'vn',887:'ye',894:'zm',716:'zw',688:'rs',
646:'rw',266:'ga',454:'mw',694:'sl',706:'so',630:'pr',388:'jm',214:'do',
496:'mn',598:'pg',84:'bz',516:'na',430:'lr',232:'er',706:'so'};

var mapMode = 'imp';
var mapExclude = <?= $top_exclude ? 'true' : 'false' ?>;
var mapStats = {};
var mapRecent = [];
var mapTimer = null;
var pulseTimer = null;
var svg, path, projection, g;
var miniSvg, miniPath, miniProjection;
var drillModal = null;
var mapExpanded = false;
var fullMapInited = false;
var now = Date.now();

function setMapMode(mode) {
    mapMode = mode;
    document.getElementById('map-mode-imp').classList.toggle('active', mode === 'imp');
    document.getElementById('map-mode-clk').classList.toggle('active', mode === 'clk');
    renderColors();
}
window.setMapMode = setMapMode;

function toggleMap(e) {
    if (e) e.stopPropagation();
    mapExpanded = !mapExpanded;
    document.getElementById('world-map-mini').style.display = mapExpanded ? 'none' : 'block';
    document.getElementById('world-map-full').style.display = mapExpanded ? 'block' : 'none';
    document.getElementById('map-chevron').style.transform = mapExpanded ? 'rotate(180deg)' : '';
    if (mapExpanded && !fullMapInited) {
        fullMapInited = true;
        initFullMap(window._mapWorld);
    } else if (mapExpanded) {
        renderPulses();
    }
}
window.toggleMap = toggleMap;

function initMiniMap(world) {
    var el = document.getElementById('world-map-mini');
    var W = el.offsetWidth, H = 460;
    miniSvg = d3.select('#world-map-mini').append('svg')
        .attr('width', W).attr('height', H).style('display','block');
    miniProjection = d3.geoNaturalEarth1()
        .scale(Math.min(W / 5.71, H / 2.91))
        .translate([W / 2, H / 2]);
    miniPath = d3.geoPath().projection(miniProjection);
    var mg = miniSvg.append('g');
    mg.append('rect').attr('width', W).attr('height', H).attr('fill','#c8d8e8');
    var countries = topojson.feature(world, world.objects.countries).features;
    mg.selectAll('.mc').data(countries).join('path')
        .attr('class','mc').attr('d', miniPath)
        .attr('data-id', function(d){ return N2A[+d.id]||''; })
        .attr('fill','#b8c8d8').attr('stroke','#fff').attr('stroke-width',.3);
}

function renderMiniColors() {
    if (!miniSvg) return;
    var vals = Object.values(mapStats).map(function(s){ return s[mapMode]||0; });
    var maxVal = vals.length ? d3.max(vals) : 1;
    miniSvg.selectAll('.mc').attr('fill', function(d){
        return countryColor(N2A[+d.id]||'', maxVal);
    });
}

function initFullMap(world) {
    var el = document.getElementById('world-map');
    var W = el.offsetWidth, H = el.offsetHeight;
    svg = d3.select('#world-map').append('svg')
        .attr('width', W).attr('height', H).style('display','block');
    projection = d3.geoNaturalEarth1()
        .scale(Math.min(W / 5.71, H / 2.91))
        .translate([W / 2, H / 2]);
    path = d3.geoPath().projection(projection);
    g = svg.append('g');
    g.append('rect').attr('width', W).attr('height', H).attr('fill','#c8d8e8').attr('rx',4);
    g.append('path').datum(d3.geoGraticule()())
        .attr('d', path).attr('fill','none')
        .attr('stroke','rgba(255,255,255,0.25)').attr('stroke-width',.3);
    var countries = topojson.feature(world, world.objects.countries).features;
    g.selectAll('.country').data(countries).join('path')
        .attr('class','country').attr('d', path)
        .attr('data-id', function(d){ return N2A[+d.id]||''; })
        .attr('fill','#b8c8d8').attr('stroke','#fff').attr('stroke-width',.4)
        .style('cursor','pointer')
        .on('mouseover', onCountryHover)
        .on('mousemove', onCountryMove)
        .on('mouseout',  onCountryOut)
        .on('click',     onCountryClick);
    svg.append('g').attr('id','pulse-layer');
    if (!document.getElementById('map-tt')) {
        var tt = document.createElement('div');
        tt.id = 'map-tt';
        tt.style.cssText = 'display:none;position:fixed;z-index:9999;background:rgba(15,20,40,.88);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;line-height:1.6;box-shadow:0 4px 16px rgba(0,0,0,.3)';
        document.body.appendChild(tt);
    }
    svg.call(d3.zoom().scaleExtent([1,8]).on('zoom', function(e){ g.attr('transform', e.transform); }));
    renderColors();
    renderPulses();
}

function countryColor(cc, maxVal) {
    if (!cc || !mapStats[cc]) return '#b8c8d8';
    var val = mapStats[cc][mapMode] || 0;
    if (!val || !maxVal) return '#b8c8d8';
    var t = Math.pow(val / maxVal, 0.4);
    return d3.interpolate('#c8dff0', '#1a4a8a')(t);
}

function renderColors() {
    var vals = Object.values(mapStats).map(function(s){ return s[mapMode] || 0; });
    var maxVal = vals.length ? d3.max(vals) : 1;
    if (g) g.selectAll('.country').attr('fill', function(d){
        return countryColor(N2A[+d.id] || '', maxVal);
    });
    renderMiniColors();
}

function renderPulses() {
    if (!svg) return;
    var pl = d3.select('#pulse-layer');
    pl.selectAll('*').remove();

    now = Date.now();
    var WINDOW = 30 * 60 * 1000; // 30 мин

    // Группируем по стране — берём последнее событие
    var byCC = {};
    mapRecent.forEach(function(r){
        var age = now - r.ts * 1000;
        if (age > WINDOW) return;
        if (!byCC[r.cc] || r.ts > byCC[r.cc].ts) byCC[r.cc] = r;
    });

    Object.values(byCC).forEach(function(r){
        var age = now - r.ts * 1000;
        var intensity = 1 - age / WINDOW;
        var centroid = getCentroid(r.cc);
        if (!centroid) return;

        var x = centroid[0], y = centroid[1];
        var color = r.type === 'click' ? '#ff6b35' : '#22c55e';
        var sz = 6 + intensity * 5;

        // Статичный кружок
        pl.append('circle')
            .attr('cx', x).attr('cy', y).attr('r', sz / 2)
            .attr('fill', color).attr('opacity', 0.7 * intensity + 0.2)
            .attr('pointer-events','none');

        // Пульс
        var pulse = pl.append('circle')
            .attr('cx', x).attr('cy', y).attr('r', sz / 2)
            .attr('fill','none').attr('stroke', color)
            .attr('stroke-width', 1.5).attr('opacity', 0.8)
            .attr('pointer-events','none');

        (function animate(circ){
            circ.attr('r', sz / 2).attr('opacity', 0.8)
                .transition().duration(1600).ease(d3.easeLinear)
                .attr('r', sz / 2 + 14).attr('opacity', 0)
                .on('end', function(){ animate(circ); });
        })(pulse);
    });
}

// Центроиды стран (приближённые, для отображения пульса)
var CENTROIDS = {
    af:[66,33],al:[20,41],dz:[3,28],ao:[18,-12],ar:[-64,-34],au:[134,-25],
    at:[14,47],az:[47,40],bd:[90,24],by:[28,53],be:[4,51],bo:[-65,-17],
    ba:[17,44],br:[-52,-10],bg:[25,43],kh:[105,13],cm:[12,6],ca:[-96,56],
    cl:[-71,-30],cn:[105,35],co:[-74,4],cr:[-84,10],hr:[16,45],cu:[-80,22],
    cy:[33,35],cz:[16,50],dk:[10,56],ec:[-78,-2],eg:[30,27],sv:[-89,14],
    ee:[25,59],et:[40,8],fi:[26,64],fr:[2,46],ge:[44,42],de:[10,51],
    gh:[-1,8],gr:[22,39],gt:[-90,15],hn:[-87,15],hu:[19,47],in:[78,20],
    id:[120,-5],ir:[53,32],iq:[44,33],ie:[-8,53],il:[35,31],it:[12,43],
    jp:[138,36],jo:[36,31],kz:[67,48],ke:[38,0],kr:[128,37],kw:[47,29],
    kg:[74,41],la:[103,18],lb:[36,34],lv:[25,57],ly:[17,27],lt:[24,56],
    mk:[21,42],my:[110,3],mx:[-102,24],mn:[105,46],ma:[-7,32],mz:[35,-18],
    np:[84,28],nl:[5,52],nz:[174,-41],ni:[-85,13],ng:[8,10],no:[10,62],
    om:[57,22],pk:[70,30],pa:[-80,9],py:[-58,-23],pe:[-76,-10],ph:[122,13],
    pl:[20,52],pt:[-8,39],qa:[51,25],ro:[25,46],ru:[100,60],sa:[45,24],
    sn:[-14,14],za:[25,-29],es:[-4,40],lk:[81,8],sd:[30,15],se:[18,62],
    ch:[8,47],sy:[38,35],tj:[71,39],tz:[35,-6],th:[101,15],tr:[35,39],
    tm:[59,40],ug:[32,1],ua:[32,49],ae:[54,24],gb:[-2,54],us:[-98,38],
    uy:[-56,-33],uz:[63,41],ve:[-66,8],vn:[108,14],ye:[48,15],zm:[28,-14],
    zw:[30,-20],rs:[21,44],rw:[30,-2],ge:[44,42],sk:[19,49],si:[15,46],
    md:[29,47],lt:[24,56],hr:[16,45],ba:[17,44],me:[19,43],al:[20,41],
    mk:[21,42],xk:[21,43],by:[28,53],am:[45,40],az:[47,40]
};

function getCentroid(cc) {
    if (!cc) return null;
    if (CENTROIDS[cc]) {
        return projection(CENTROIDS[cc]);
    }
    // Fallback: find path and compute centroid
    var found = null;
    g && g.selectAll('.country').each(function(d){
        if (N2A[+d.id] === cc) {
            found = path.centroid(d);
        }
    });
    return found;
}

var ttEl;
function onCountryHover(e, d) {
    var cc = N2A[+d.id] || '';
    if (!cc) return;
    ttEl = document.getElementById('map-tt');
    var s = mapStats[cc] || {imp:0,clk:0};
    var ctr = s.imp ? Math.round(s.clk/s.imp*1000)/10 + '%' : '—';
    var flag = '<img src="https://flagicons.lipis.dev/flags/4x3/'+cc+'.svg" width="16" height="12" style="border-radius:2px;vertical-align:middle;margin-right:4px">';
    ttEl.innerHTML = flag + '<strong><?php echo ""; ?>' + cc.toUpperCase() + '</strong><br>'
        + '👁 Показы: <b>' + (s.imp||0).toLocaleString() + '</b><br>'
        + '🖱 Клики: <b>' + (s.clk||0).toLocaleString() + '</b><br>'
        + '📊 CTR: <b>' + ctr + '</b>';
    ttEl.style.display = 'block';
    d3.select(this).attr('stroke','#ff6b00').attr('stroke-width',1.5);
}
function onCountryMove(e) {
    if (!ttEl) return;
    ttEl.style.left = (e.clientX + 14) + 'px';
    ttEl.style.top  = (e.clientY - 10) + 'px';
}
function onCountryOut() {
    if (ttEl) ttEl.style.display = 'none';
    d3.select(this).attr('stroke','#fff').attr('stroke-width',.4);
}

function onCountryClick(e, d) {
    var cc = N2A[+d.id] || '';
    if (!cc) return;
    if (ttEl) ttEl.style.display = 'none';
    openDrill(cc);
}

function openDrill(cc) {
    var s = mapStats[cc] || {imp:0,clk:0};
    var ctr = s.imp ? Math.round(s.clk/s.imp*1000)/10 + '%' : '0%';
    var flag = '<img src="https://flagicons.lipis.dev/flags/4x3/'+cc+'.svg" width="20" height="15" style="border-radius:2px;vertical-align:middle;margin-right:6px">';
    document.getElementById('mapDrillTitle').innerHTML = flag + cc.toUpperCase();
    var body = document.getElementById('mapDrillBody');
    body.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-secondary"></div></div>';

    if (!drillModal) drillModal = new bootstrap.Modal(document.getElementById('mapDrillModal'));
    drillModal.show();

    fetch('/admin/map-data.php?cc=' + cc + (mapExclude ? '&exclude_cnus=1' : ''))
        .then(function(r){ return r.json(); })
        .then(function(data) {
            var drill = data.drill || {};
            var brands = drill.brands || [];
            var days   = drill.days   || [];

            var html = '<div class="row g-3 mb-3">'
                + '<div class="col-4"><div class="card text-center p-2"><div class="fw-bold fs-5">'+s.imp.toLocaleString()+'</div><div class="text-muted small">Показы</div></div></div>'
                + '<div class="col-4"><div class="card text-center p-2"><div class="fw-bold fs-5">'+s.clk.toLocaleString()+'</div><div class="text-muted small">Клики</div></div></div>'
                + '<div class="col-4"><div class="card text-center p-2"><div class="fw-bold fs-5">'+ctr+'</div><div class="text-muted small">CTR</div></div></div>'
                + '</div>';

            if (brands.length) {
                html += '<h6 class="text-muted fw-semibold mb-2">Топ брендов</h6><div class="table-responsive"><table class="table table-sm align-middle mb-3"><thead class="table-light"><tr><th>Бренд</th><th class="text-center">Показы</th><th class="text-center">Клики</th><th class="text-center">CTR</th></tr></thead><tbody>';
                brands.forEach(function(b){
                    var bctr = b.imp ? Math.round(b.clk/b.imp*1000)/10+'%' : '—';
                    var logo = b.logo ? '<img src="'+b.logo+'" style="height:20px;max-width:50px;object-fit:contain;vertical-align:middle;margin-right:6px">' : '';
                    html += '<tr><td>'+logo+b.name+'</td><td class="text-center">'+b.imp+'</td><td class="text-center">'+b.clk+'</td><td class="text-center">'+bctr+'</td></tr>';
                });
                html += '</tbody></table></div>';
            }

            if (days.length > 1) {
                var labels = days.map(function(d){ return d.day.slice(5); });
                var impD   = days.map(function(d){ return +d.imp; });
                var clkD   = days.map(function(d){ return +d.clk; });
                html += '<h6 class="text-muted fw-semibold mb-2">Последние 14 дней</h6><div style="height:160px;position:relative"><canvas id="drillChart"></canvas></div>';
                body.innerHTML = html;
                new Chart(document.getElementById('drillChart'), {
                    type:'bar',
                    data:{ labels:labels, datasets:[
                        {label:'Показы',data:impD,backgroundColor:'rgba(59,130,246,.5)',borderColor:'#3b82f6',borderWidth:1},
                        {label:'Клики', data:clkD,backgroundColor:'rgba(239,68,68,.5)', borderColor:'#ef4444',borderWidth:1}
                    ]},
                    options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{boxWidth:12,font:{size:11}}}},scales:{x:{ticks:{font:{size:10}}},y:{ticks:{font:{size:10}}}}}
                });
                return;
            }

            body.innerHTML = html;
        })
        .catch(function(){ body.innerHTML = '<div class="text-danger small">Ошибка загрузки</div>'; });
}

function fetchMapData() {
    fetch('/admin/map-data.php' + (mapExclude ? '?exclude_cnus=1' : ''))
        .then(function(r){ return r.json(); })
        .then(function(data){
            mapStats  = data.stats  || {};
            mapRecent = data.recent || [];
            renderColors();
            renderPulses();
            // Мигание LIVE-бейджа
            var badge = document.getElementById('map-live');
            badge.style.opacity = '0';
            setTimeout(function(){ badge.style.opacity = '1'; }, 200);
        });
}

// Загрузка карты
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
    .then(function(r){ return r.json(); })
    .then(function(world){
        window._mapWorld = world;
        initMiniMap(world);
        fetchMapData();
        setInterval(fetchMapData, 30000);
    });

})();
</script>
</body>
</html>