diff --git a/admin/manual_add.php b/admin/manual_add.php
new file mode 100644
index 0000000..d221251
--- /dev/null
+++ b/admin/manual_add.php
@@ -0,0 +1,249 @@
+query('SELECT id, country_id, mcc, mnc, brand FROM operators ORDER BY brand')->fetchAll(PDO::FETCH_ASSOC);
+$successMessage = null;
+$errorMessage = null;
+
+function composeLocationDesc(string $tech, ?string $enb, ?string $band, ?string $frequency, $azimuth, ?string $bandwidth, string $address): string
+{
+ $techLabel = ['2G' => 'GSM', '3G' => 'UMTS', '4G' => 'LTE', '5G' => 'NR', 'CDMA' => 'CDMA'][$tech] ?? $tech;
+ $segments = [];
+
+ if ($enb !== null && $enb !== '') {
+ $segments[] = "eNB ID $enb";
+ }
+
+ $radioLine = trim($techLabel . ' ' . ($band ?? '') . ' ' . ($frequency ?? ''));
+ if ($radioLine !== $techLabel) {
+ $segments[] = $radioLine;
+ }
+
+ if ($azimuth !== null && $azimuth !== '') {
+ $segments[] = $azimuth . '°';
+ }
+
+ if ($bandwidth !== null && $bandwidth !== '') {
+ $segments[] = $bandwidth;
+ }
+
+ if ($address !== '') {
+ $segments[] = $address;
+ }
+
+ return implode(' - ', $segments);
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ require_csrf_token();
+
+ $operatorId = (int)($_POST['operator_id'] ?? 0);
+ $lat = (float)($_POST['lat'] ?? 0);
+ $lon = (float)($_POST['lon'] ?? 0);
+ $addressDesc = trim($_POST['address_desc'] ?? '');
+ $batchLabel = trim($_POST['batch_label'] ?? ('Saisie manuelle - ' . $addressDesc));
+
+ $techs = $_POST['cell_tech'] ?? [];
+ $cellIds = $_POST['cell_id'] ?? [];
+ $areaCodes = $_POST['cell_area_code'] ?? [];
+ $subIds = $_POST['cell_sub_id'] ?? [];
+ $subIds2 = $_POST['cell_sub_id2'] ?? [];
+ $arfcns = $_POST['cell_arfcn'] ?? [];
+ $bands = $_POST['cell_band'] ?? [];
+ $frequencies = $_POST['cell_frequency'] ?? [];
+ $azimuths = $_POST['cell_azimuth'] ?? [];
+ $bandwidths = $_POST['cell_bandwidth'] ?? [];
+
+ if (!$operatorId || !$lat || !$lon || empty($techs)) {
+ $errorMessage = "Operateur, coordonnees et au moins une cellule sont obligatoires.";
+ } else {
+ $opStmt = $db->prepare('SELECT mcc, mnc FROM operators WHERE id = ?');
+ $opStmt->execute([$operatorId]);
+ $op = $opStmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$op) {
+ $errorMessage = "Operateur invalide.";
+ } else {
+ $db->beginTransaction();
+ try {
+ $batchStmt = $db->prepare('INSERT INTO staging_batches (label, created_by) VALUES (?, ?)');
+ $batchStmt->execute([$batchLabel, $user]);
+ $batchId = (int)$db->lastInsertId();
+
+ $insertStmt = $db->prepare('
+ INSERT INTO staging_cells
+ (batch_id, tech, mcc, mnc, operator_guess, cell_id, area_code, sub_id, sub_id2,
+ band, frequency, azimuth, bandwidth, address_desc,
+ lat, lon, location_desc, arfcn, source, status)
+ VALUES
+ (:batch_id, :tech, :mcc, :mnc, :operator_guess, :cell_id, :area_code, :sub_id, :sub_id2,
+ :band, :frequency, :azimuth, :bandwidth, :address_desc,
+ :lat, :lon, :location_desc, :arfcn, "manual_form", "pending")
+ ');
+
+ $count = 0;
+ foreach ($techs as $i => $tech) {
+ if ($tech === '' || $cellIds[$i] === '' || $areaCodes[$i] === '') continue;
+
+ $enb = $subIds2[$i] !== '' ? $subIds2[$i] : null;
+ $band = $bands[$i] !== '' ? $bands[$i] : null;
+ $frequency = $frequencies[$i] !== '' ? $frequencies[$i] : null;
+ $azimuth = $azimuths[$i] !== '' ? (int)$azimuths[$i] : null;
+ $bandwidth = $bandwidths[$i] !== '' ? $bandwidths[$i] : null;
+
+ $composedLocation = composeLocationDesc($tech, $enb, $band, $frequency, $azimuth, $bandwidth, $addressDesc);
+
+ $insertStmt->execute([
+ ':batch_id' => $batchId, ':tech' => $tech, ':mcc' => $op['mcc'], ':mnc' => $op['mnc'], ':operator_guess' => $operatorId,
+ ':cell_id' => $cellIds[$i], ':area_code' => $areaCodes[$i],
+ ':sub_id' => $subIds[$i] !== '' ? $subIds[$i] : null, ':sub_id2' => $enb,
+ ':band' => $band, ':frequency' => $frequency, ':azimuth' => $azimuth, ':bandwidth' => $bandwidth,
+ ':address_desc' => $addressDesc,
+ ':lat' => $lat, ':lon' => $lon, ':location_desc' => $composedLocation,
+ ':arfcn' => $arfcns[$i] !== '' ? $arfcns[$i] : null,
+ ]);
+ $count++;
+ }
+ $db->commit();
+ $successMessage = "$count cellule(s) envoyee(s) en validation (lot #$batchId).";
+ } catch (Throwable $e) {
+ $db->rollBack();
+ $errorMessage = "Erreur : " . $e->getMessage();
+ }
+ }
+ }
+}
+?>
+
+
+
+NetMonster Admin - Ajout manuel
+
+
+
+
+
+
+
+
Ajout manuel d'un site
+
Un site peut porter plusieurs cellules/secteurs, chacune avec sa propre bande, frequence, azimut et largeur de bande.
+
+
= htmlspecialchars($successMessage) ?>
+
= htmlspecialchars($errorMessage) ?>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/map_layers.php b/admin/map_layers.php
new file mode 100644
index 0000000..cf66cdc
--- /dev/null
+++ b/admin/map_layers.php
@@ -0,0 +1,197 @@
+prepare(
+ 'INSERT INTO map_layers (name, layer_key, color, is_visible, sort_order, description)
+ VALUES (?, ?, ?, 1, (SELECT COALESCE(MAX(sort_order), 0) + 1 FROM map_layers AS m2), ?)'
+ );
+ $stmt->execute([$name, $layerKey, $color, $description]);
+ $feedback = 'Couche créée.';
+ } catch (Throwable $e) {
+ $feedback = 'Erreur lors de la création (clé·· déjà utilisée ?) : ' . $e->getMessage();
+ $feedbackType = 'error';
+ }
+ }
+ } elseif ($action === 'toggle_visibility') {
+ $layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
+ if ($layerId) {
+ $stmt = $db->prepare('UPDATE map_layers SET is_visible = 1 - is_visible WHERE id = ?');
+ $stmt->execute([$layerId]);
+ $feedback = 'Visibilité·· mise à jour.';
+ }
+ } elseif ($action === 'update_color') {
+ $layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
+ $color = sanitize_hex_color((string)($_POST['color'] ?? ''));
+ if ($layerId && $color !== null) {
+ $stmt = $db->prepare('UPDATE map_layers SET color = ? WHERE id = ?');
+ $stmt->execute([$color, $layerId]);
+ $feedback = 'Couleur mise à jour.';
+ } else {
+ $feedback = 'Couleur invalide.';
+ $feedbackType = 'error';
+ }
+ } elseif ($action === 'delete_layer') {
+ $layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
+ $confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
+ if (!$confirmed || !$layerId) {
+ $feedback = 'Suppression refusé··e : confirmation manquante.';
+ $feedbackType = 'error';
+ } else {
+ $stmt = $db->prepare('DELETE FROM map_layers WHERE id = ? LIMIT 1');
+ $stmt->execute([$layerId]);
+ $feedback = $stmt->rowCount() > 0 ? 'Couche supprimé··e.' : 'Couche introuvable.';
+ }
+ }
+}
+
+$layers = $db->query('SELECT id, name, layer_key, color, is_visible, sort_order, description FROM map_layers ORDER BY sort_order ASC')->fetchAll(PDO::FETCH_ASSOC);
+$user = current_admin_username();
+?>
+
+
+
+
+ Couches cartographiques — NetMonster Admin
+
+
+
+
+
+
+ Couches cartographiques
+ Gé··rez les couches affich ées sur la carte (visibilité··, couleur, ordre). Toute suppression demande une confirmation explicite.
+
+
+
+ = htmlspecialchars($feedback, ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+
+
+ Couches existantes (= count($layers) ?>)
+
+
+
+ | Ordre |
+ Nom |
+ Clé·· |
+ Couleur |
+ Visible |
+ Description |
+ Actions |
+
+
+
+
+
+ | = (int)$layer['sort_order'] ?> |
+ = htmlspecialchars((string)$layer['name'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)$layer['layer_key'], ENT_QUOTES, 'UTF-8') ?> |
+
+
+ |
+
+
+ |
+ = htmlspecialchars((string)$layer['description'], ENT_QUOTES, 'UTF-8') ?> |
+
+
+ |
+
+
+
+ | Aucune couche configuré··e. |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/operator_sites.php b/admin/operator_sites.php
new file mode 100644
index 0000000..e671a63
--- /dev/null
+++ b/admin/operator_sites.php
@@ -0,0 +1,185 @@
+query('SELECT id, name FROM countries ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
+
+$countryId = filter_input(INPUT_GET, 'country_id', FILTER_VALIDATE_INT) ?: null;
+
+$operatorsSql = 'SELECT id, name, mcc, mnc, country_id FROM operators';
+$operatorsParams = [];
+if ($countryId) {
+ $operatorsSql .= ' WHERE country_id = ?';
+ $operatorsParams[] = $countryId;
+}
+$operatorsSql .= ' ORDER BY name';
+$stmt = $db->prepare($operatorsSql);
+$stmt->execute($operatorsParams);
+$operators = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$operatorId = filter_input(INPUT_GET, 'operator_id', FILTER_VALIDATE_INT) ?: null;
+
+$page = max(1, filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1);
+$perPage = 50;
+$offset = ($page - 1) * $perPage;
+
+$sites = [];
+$totalSites = 0;
+$stats = null;
+
+if ($operatorId) {
+ $countStmt = $db->prepare('SELECT COUNT(*) FROM sites WHERE operator_id = ?');
+ $countStmt->execute([$operatorId]);
+ $totalSites = (int)$countStmt->fetchColumn();
+
+ $stmt = $db->prepare(
+ 'SELECT s.id, s.name, s.lat, s.lon,
+ (SELECT COUNT(*) FROM cells c WHERE c.site_id = s.id) AS cell_count
+ FROM sites s
+ WHERE s.operator_id = ?
+ ORDER BY s.name
+ LIMIT :limit OFFSET :offset'
+ );
+ $stmt->bindValue(1, $operatorId, PDO::PARAM_INT);
+ $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
+ $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+ $stmt->execute();
+ $sites = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $statStmt = $db->prepare(
+ "SELECT c.technology, COUNT(*) AS total
+ FROM cells c
+ INNER JOIN sites s ON s.id = c.site_id
+ WHERE s.operator_id = ?
+ GROUP BY c.technology
+ ORDER BY total DESC"
+ );
+ $statStmt->execute([$operatorId]);
+ $stats = $statStmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+$totalPages = max(1, (int)ceil($totalSites / $perPage));
+$user = current_admin_username();
+?>
+
+
+
+
+ Antennes par opé··rateur — NetMonster Admin
+
+
+
+
+
+
+ Antennes par opé··rateur
+ Vue de consultation, sans action d'ecriture. Filtrez par pays puis par operateur pour lister les sites et leur nombre de cellules.
+
+
+
+
+
+ Ré··partition par technologie
+
+ Aucune cellule pour cet opé··rateur.
+
+
+ | Technologie | Nombre de cellules |
+
+
+
+ | = htmlspecialchars((string)$s['technology'], ENT_QUOTES, 'UTF-8') ?> |
+ = (int)$s['total'] ?> |
+
+
+
+
+
+
+
+
+ Sites (= $totalSites ?>)
+
+
+
+ | Nom |
+ Latitude |
+ Longitude |
+ Cellules |
+ Dé··tail |
+
+
+
+
+
+ | = htmlspecialchars((string)$site['name'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)$site['lat'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)$site['lon'], ENT_QUOTES, 'UTF-8') ?> |
+ = (int)$site['cell_count'] ?> |
+ Ouvrir |
+
+
+
+ | Aucun site pour cet opé··rateur. |
+
+
+
+
+
+
+
+
+ Sé··lectionnez un opé··rateur pour afficher ses sites.
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/review.php b/admin/review.php
new file mode 100644
index 0000000..aca305e
--- /dev/null
+++ b/admin/review.php
@@ -0,0 +1,383 @@
+prepare('SELECT country_id FROM operators WHERE id = ?');
+ $opCheck->execute([$operatorId]);
+ $countryId = $opCheck->fetchColumn();
+
+ if ($stagingIds && $operatorId && $countryId !== false) {
+ $db->beginTransaction();
+ try {
+ $near = $db->prepare('SELECT id FROM sites WHERE operator_id = ? AND ABS(lat - ?) < 0.0003 AND ABS(lon - ?) < 0.0003 LIMIT 1');
+ $near->execute([$operatorId, $lat, $lon]);
+ $siteId = $near->fetchColumn();
+
+ if (!$siteId) {
+ $addrStmt = $db->prepare('SELECT address_desc FROM staging_cells WHERE id = ? LIMIT 1');
+ $addrStmt->execute([$stagingIds[0]]);
+ $addressDesc = $addrStmt->fetchColumn() ?: null;
+
+ $insertSite = $db->prepare('INSERT INTO sites (country_id, operator_id, lat, lon, location_desc, address_desc, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)');
+ $insertSite->execute([$countryId, $operatorId, $lat, $lon, $locationDesc, $addressDesc, $user]);
+ $siteId = (int)$db->lastInsertId();
+ }
+
+ $fetchStaging = $db->prepare('SELECT * FROM staging_cells WHERE id = ? AND status = "pending"');
+ $insertCell = $db->prepare('
+ INSERT INTO cells (site_id, tech, mcc, mnc, cell_id, area_code, sub_id, sub_id2, band, frequency, azimuth, bandwidth, arfcn, validated_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ site_id = VALUES(site_id), band = VALUES(band), frequency = VALUES(frequency),
+ azimuth = VALUES(azimuth), bandwidth = VALUES(bandwidth),
+ validated_by = VALUES(validated_by), validated_at = NOW()
+ ');
+ $markValidated = $db->prepare('UPDATE staging_cells SET status = "validated", reviewed_at = NOW(), reviewed_by = ? WHERE id = ?');
+
+ foreach ($stagingIds as $sid) {
+ $fetchStaging->execute([$sid]);
+ $cell = $fetchStaging->fetch(PDO::FETCH_ASSOC);
+ if (!$cell) continue;
+ $insertCell->execute([
+ $siteId, $cell['tech'], $cell['mcc'], $cell['mnc'], $cell['cell_id'], $cell['area_code'],
+ $cell['sub_id'], $cell['sub_id2'], $cell['band'], $cell['frequency'], $cell['azimuth'], $cell['bandwidth'],
+ $cell['arfcn'], $user,
+ ]);
+ $markValidated->execute([$user, $sid]);
+ }
+ $db->commit();
+ } catch (Throwable $e) {
+ $db->rollBack();
+ // Corrige : l'echec n'est plus silencieux. On loggue le
+ // detail technique cote serveur et on informe
+ // l'administrateur qu'aucune validation n'a ete
+ // appliquee, plutot que de rediriger comme si de rien
+ // n'etait alors que la transaction a echoue.
+ error_log('[review] validate_group: ' . $e->getMessage());
+ $flashError = "La validation du site a echoue et a ete annulee (aucune donnee ecrite). Consultez les journaux du serveur si le probleme persiste.";
+ }
+ } else {
+ $flashError = "Validation refusee : operateur invalide ou selection de cellules vide.";
+ }
+ }
+
+ if ($action === 'reject') {
+ $stagingId = (int)($_POST['staging_id'] ?? 0);
+ try {
+ $db->prepare('UPDATE staging_cells SET status = "rejected", reviewed_at = NOW(), reviewed_by = ?, reject_reason = ? WHERE id = ? AND status = "pending"')
+ ->execute([$user, $_POST['reject_reason'] ?? null, $stagingId]);
+ } catch (Throwable $e) {
+ error_log('[review] reject: ' . $e->getMessage());
+ $flashError = 'Le rejet de cette cellule a echoue.';
+ }
+ }
+
+ if ($action === 'add_missing_cell') {
+ // Verifie que l'operateur/pays declares existent reellement (coherence
+ // MCC/MNC) avant d'inserer une nouvelle ligne staging manuelle.
+ $operatorId = (int)($_POST['operator_id'] ?? 0);
+ $opCheck = $db->prepare('SELECT mcc, mnc FROM operators WHERE id = ?');
+ $opCheck->execute([$operatorId]);
+ $op = $opCheck->fetch(PDO::FETCH_ASSOC);
+
+ if ($op && in_array($_POST['tech'] ?? '', ['2G', '3G', '4G', '5G', 'CDMA'], true)) {
+ try {
+ $db->prepare('
+ INSERT INTO staging_cells (tech, mcc, mnc, operator_guess, cell_id, area_code, sub_id, sub_id2, band, frequency, azimuth, bandwidth, lat, lon, location_desc, arfcn, source, status)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "manual_form", "pending")
+ ')->execute([
+ $_POST['tech'], $op['mcc'], $op['mnc'], $operatorId,
+ $_POST['cell_id'], $_POST['area_code'],
+ $_POST['sub_id'] !== '' ? $_POST['sub_id'] : null, $_POST['sub_id2'] !== '' ? $_POST['sub_id2'] : null,
+ $_POST['band'] !== '' ? $_POST['band'] : null, $_POST['frequency'] !== '' ? $_POST['frequency'] : null,
+ $_POST['azimuth'] !== '' ? (int)$_POST['azimuth'] : null, $_POST['bandwidth'] !== '' ? $_POST['bandwidth'] : null,
+ $_POST['lat'], $_POST['lon'], $_POST['location_desc'], $_POST['arfcn'] !== '' ? $_POST['arfcn'] : null,
+ ]);
+ } catch (Throwable $e) {
+ error_log('[review] add_missing_cell: ' . $e->getMessage());
+ $flashError = "L'ajout de la cellule manquante a echoue.";
+ }
+ } else {
+ $flashError = "Ajout refuse : operateur ou technologie invalide.";
+ }
+ }
+
+ if ($flashError !== null) {
+ $_SESSION['review_flash_error'] = $flashError;
+ }
+
+ header('Location: review.php' . (isset($_GET['country']) ? '?country=' . urlencode($_GET['country']) : ''));
+ exit;
+}
+
+$flashError = $_SESSION['review_flash_error'] ?? null;
+unset($_SESSION['review_flash_error']);
+
+$countryFilter = $_GET['country'] ?? ''; $operatorFilter = $_GET['operator'] ?? '';
+
+$sql = '
+ SELECT sc.*, o.brand AS operator_brand, c.name AS country_name
+ FROM staging_cells sc
+ LEFT JOIN operators o ON o.id = sc.operator_guess
+ LEFT JOIN countries c ON c.mcc = sc.mcc
+ WHERE sc.status = "pending"
+';
+$params = [];
+if ($countryFilter !== '') { $sql .= ' AND c.iso2 = ?'; $params[] = $countryFilter; }
+if ($operatorFilter !== '') { $sql .= ' AND sc.operator_guess = ?'; $params[] = $operatorFilter; }
+$sql .= ' ORDER BY sc.submitted_at DESC LIMIT 500';
+
+$stmt = $db->prepare($sql); $stmt->execute($params);
+$allPending = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$groups = [];
+foreach ($allPending as $row) {
+ $key = deriveSiteKey($row);
+ $groups[$key]['cells'][] = $row;
+ $groups[$key]['operator_brand'] = $row['operator_brand'];
+ $groups[$key]['operator_id'] = $row['operator_guess'];
+ $groups[$key]['mcc'] = $row['mcc']; $groups[$key]['mnc'] = $row['mnc'];
+ $groups[$key]['lat'] = $groups[$key]['lat'] ?? $row['lat'];
+ $groups[$key]['lon'] = $groups[$key]['lon'] ?? $row['lon'];
+ $groups[$key]['location_desc'] = $groups[$key]['location_desc'] ?? $row['location_desc'];
+ $groups[$key]['country_name'] = $row['country_name'];
+ $groups[$key]['has_dup'] = ($groups[$key]['has_dup'] ?? false) || (bool)$row['possible_dup_of'];
+}
+
+$operators = $db->query('SELECT id, mcc, mnc, brand FROM operators ORDER BY brand')->fetchAll(PDO::FETCH_ASSOC);
+$countries = $db->query('SELECT iso2, name, mcc, default_lat, default_lon, default_zoom, cadastre_wms_url, cadastre_wms_layer, cadastre_attribution_html, cadastre_srs FROM countries ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
+
+$activeCountry = null;
+if ($countryFilter !== '') {
+ foreach ($countries as $c) { if ($c['iso2'] === $countryFilter) { $activeCountry = $c; break; } }
+} elseif (!empty($groups)) {
+ $firstMcc = reset($groups)['mcc'];
+ foreach ($countries as $c) { if ($c['mcc'] === $firstMcc) { $activeCountry = $c; break; } }
+}
+if (!$activeCountry) {
+ $activeCountry = ['default_lat' => 20, 'default_lon' => 0, 'default_zoom' => 2, 'cadastre_wms_url' => null, 'cadastre_wms_layer' => null, 'cadastre_attribution_html' => null, 'cadastre_srs' => 'EPSG:3857'];
+}
+
+$mapPoints = [];
+foreach ($groups as $key => $g) {
+ if ($g['lat'] && $g['lon']) {
+ $mapPoints[] = ['key' => $key, 'lat' => (float)$g['lat'], 'lon' => (float)$g['lon'], 'operator' => $g['operator_brand'], 'count' => count($g['cells']), 'has_dup' => $g['has_dup']];
+ }
+}
+$hasCadastre = !empty($activeCountry['cadastre_wms_url']) && !empty($activeCountry['cadastre_wms_layer']);
+$totalPending = count($allPending);
+$totalDup = count(array_filter($allPending, fn($r) => (bool)$r['possible_dup_of']));
+?>
+
+
+
+NetMonster Admin - Validation
+
+
+
+
+
+
+
+
+
Validation groupee par site
+
Verifiez chaque site avant qu'il ne rejoigne la base finale.
+
+
= htmlspecialchars($flashError, ENT_QUOTES, 'UTF-8') ?>
+
+
+
= count($groups) ?>site(s) en attente
+
= $totalPending ?>ligne(s) au total
+
= $totalDup ?>doublon(s) potentiel(s)
+
+
+
+
+
+
+
+
+
+ $group): ?>
+
+
+
+ | Techno | Cell ID | Area code | Sub ID | eNB/RNC | Bande | Frequence | Azimut | Largeur | ARFCN | Dup. | |
+
+
+ | = htmlspecialchars((string)$c['tech'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)$c['cell_id'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)$c['area_code'], ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)($c['sub_id'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)($c['sub_id2'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)($c['band'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)($c['frequency'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = $c['azimuth'] !== null ? htmlspecialchars((string)$c['azimuth'], ENT_QUOTES, 'UTF-8') . '°' : '' ?> |
+ = htmlspecialchars((string)($c['bandwidth'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = htmlspecialchars((string)($c['arfcn'] ?? ''), ENT_QUOTES, 'UTF-8') ?> |
+ = $c['possible_dup_of'] ? '#' . htmlspecialchars((string)$c['possible_dup_of'], ENT_QUOTES, 'UTF-8') : '—' ?> |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/search_site.php b/admin/search_site.php
new file mode 100644
index 0000000..dbda92b
--- /dev/null
+++ b/admin/search_site.php
@@ -0,0 +1,454 @@
+query('SELECT id, iso2, name FROM countries ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
+$operators = $db->query('SELECT id, mcc, mnc, brand FROM operators ORDER BY brand')->fetchAll(PDO::FETCH_ASSOC);
+
+$sites = [];
+$selectedSite = null;
+$selectedCells = [];
+
+if ($query !== '' || $countryFilter !== '' || $operatorFilter !== '' || $techFilter !== '' || $selectedSiteId > 0) {
+ $sql = '
+ SELECT
+ s.id AS site_id,
+ s.lat,
+ s.lon,
+ s.location_desc,
+ s.address_desc,
+ s.country_id,
+ s.operator_id,
+ o.brand AS operator_brand,
+ o.mcc AS operator_mcc,
+ o.mnc AS operator_mnc,
+ c.name AS country_name,
+ cl.id AS cell_pk,
+ cl.mcc,
+ cl.mnc,
+ cl.tech,
+ cl.cell_id,
+ cl.area_code,
+ cl.sub_id,
+ cl.sub_id2,
+ cl.band,
+ cl.frequency,
+ cl.azimuth,
+ cl.bandwidth,
+ cl.arfcn
+ FROM sites s
+ JOIN operators o ON o.id = s.operator_id
+ JOIN countries c ON c.id = s.country_id
+ JOIN cells cl ON cl.site_id = s.id
+ WHERE 1=1
+ ';
+ $params = [];
+
+ if ($query !== '') {
+ $sql .= '
+ AND (
+ cl.sub_id2 LIKE ?
+ OR cl.cell_id LIKE ?
+ OR cl.sub_id LIKE ?
+ OR cl.band LIKE ?
+ OR cl.frequency LIKE ?
+ OR s.location_desc LIKE ?
+ OR s.address_desc LIKE ?
+ )
+ ';
+ $like = '%' . $query . '%';
+ array_push($params, $like, $like, $like, $like, $like, $like, $like);
+ }
+
+ if ($countryFilter !== '') {
+ $sql .= ' AND c.iso2 = ?';
+ $params[] = $countryFilter;
+ }
+
+ if ($operatorFilter !== '') {
+ $sql .= ' AND o.id = ?';
+ $params[] = (int)$operatorFilter;
+ }
+
+ if ($techFilter !== '') {
+ $sql .= ' AND cl.tech = ?';
+ $params[] = $techFilter;
+ }
+
+ if ($selectedSiteId > 0) {
+ $sql .= ' AND s.id = ?';
+ $params[] = $selectedSiteId;
+ }
+
+ $sql .= ' ORDER BY s.id, cl.sub_id2, cl.tech, cl.band, cl.frequency, cl.id LIMIT 1000';
+
+ $stmt = $db->prepare($sql);
+ $stmt->execute($params);
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($rows as $row) {
+ $siteId = (int)$row['site_id'];
+
+ if (!isset($sites[$siteId])) {
+ $sites[$siteId] = [
+ 'site_id' => $siteId,
+ 'lat' => (float)$row['lat'],
+ 'lon' => (float)$row['lon'],
+ 'location_desc' => (string)($row['location_desc'] ?? ''),
+ 'address_desc' => (string)($row['address_desc'] ?? ''),
+ 'operator_brand' => (string)$row['operator_brand'],
+ 'country_name' => (string)$row['country_name'],
+ 'cell_count' => 0,
+ 'cells' => [],
+ ];
+ }
+
+ $cell = [
+ 'cell_pk' => (int)$row['cell_pk'],
+ 'mcc' => (string)($row['mcc'] ?? ''),
+ 'mnc' => (string)($row['mnc'] ?? ''),
+ 'tech' => (string)($row['tech'] ?? ''),
+ 'cell_id' => (string)($row['cell_id'] ?? ''),
+ 'area_code' => (string)($row['area_code'] ?? ''),
+ 'sub_id' => (string)($row['sub_id'] ?? ''),
+ 'sub_id2' => (string)($row['sub_id2'] ?? ''),
+ 'band' => (string)($row['band'] ?? ''),
+ 'frequency' => (string)($row['frequency'] ?? ''),
+ 'azimuth' => $row['azimuth'] !== null && $row['azimuth'] !== '' ? (float)$row['azimuth'] : null,
+ 'bandwidth' => (string)($row['bandwidth'] ?? ''),
+ 'arfcn' => (string)($row['arfcn'] ?? ''),
+ ];
+
+ $sites[$siteId]['cells'][] = $cell;
+ $sites[$siteId]['cell_count']++;
+ }
+
+ $sites = array_values($sites);
+
+ if ($selectedSiteId > 0 && isset($sites[0]) && (int)$sites[0]['site_id'] === $selectedSiteId) {
+ $selectedSite = $sites[0];
+ $selectedCells = $selectedSite['cells'];
+ } elseif (count($sites) === 1) {
+ $selectedSite = $sites[0];
+ $selectedCells = $selectedSite['cells'];
+ }
+}
+
+function h(mixed $value): string
+{
+ return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
+}
+
+function techColor(string $tech): string
+{
+ return match (strtoupper($tech)) {
+ '2G' => '#eab308',
+ '3G' => '#2563eb',
+ '4G' => '#16a34a',
+ '5G' => '#9333ea',
+ 'CDMA' => '#ea580c',
+ default => '#64748b',
+ };
+}
+
+$mapSites = $selectedSite ? [$selectedSite] : $sites;
+?>
+
+
+
+
+
+NetMonster Admin - Recherche antenne
+
+
+
+
+
+
+
+
+
+
Recherche antenne / eNB
+
Recherchez un eNB, une cellule, une bande, une frequence ou une adresse. Les secteurs avec azimut sont dessines sur la carte.
+
+
+
+
+
+
+
Antennes trouvees (= count($sites) ?>)
+
+
Saisissez un eNB, un Cell ID, une bande ou une adresse.
+
+
Aucune antenne trouvee.
+
+
+
+
+
+
+
+
Cellules de l'antenne
+
= h($selectedSite['operator_brand']) ?> — = h($selectedSite['address_desc'] ?: $selectedSite['location_desc']) ?>
+
+
+ | Techno | Cell ID | eNB/RNC | Bande | Frequence | Largeur | Azimut | ARFCN |
+
+
+ | = h($cell['tech']) ?> |
+ = h($cell['cell_id']) ?> |
+ = h($cell['sub_id2']) ?> |
+ = h($cell['band']) ?> |
+ = h($cell['frequency']) ?> |
+ = h($cell['bandwidth']) ?> |
+ = $cell['azimuth'] === null ? '—' : h(rtrim(rtrim(number_format((float)$cell['azimuth'], 2, '.', ''), '0'), '.')) . '°' ?> |
+ = h($cell['arfcn']) ?> |
+
+
+
+
+
+
+
+
+
Carte des azimuts
+
+
+ = $tech ?>
+
+
+
+
+
+
+
+
\ No newline at end of file