619 lines
26 KiB
PHP
619 lines
26 KiB
PHP
<?php
|
|
/**
|
|
* admin/cellmapper_import.php
|
|
*
|
|
* Import d'un export JSON CellMapper (un site = un eNB LTE) pour
|
|
* completer/creer des cellules dans staging_cells.
|
|
*
|
|
* MODIFICATION (21/09/2026) : ajout d'un selecteur de bande 4G manuel
|
|
* avec remplissage automatique de l'EARFCN selon le mapping operateur.
|
|
* Les operateurs utilisant des EARFCN differents sur la meme bande,
|
|
* la deduction automatique depuis l'EARFCN n'est plus fiable.
|
|
*
|
|
* Regles fonctionnelles validees avec l'utilisateur :
|
|
* - CellMapper fournit uniquement des donnees RADIO fiables :
|
|
* cell_id (via "Sector"), PCI (stocke dans sub_id, comme .ntm),
|
|
* EARFCN (via "channels"), bande (deduite de l'EARFCN ou selectionnee),
|
|
* largeur de bande (via estimatedBandData / Bandwidth),
|
|
* TAC/area_code (via "regionID").
|
|
* - CellMapper NE fournit PAS de coordonnees/azimut/adresse fiables
|
|
* pour ce projet : ces champs ne sont JAMAIS ecrits ni ecrases.
|
|
* - Correspondance STRICTE sur (tech, mcc, mnc, sub_id2=eNB).
|
|
* - UPDATE des champs radio si cell_id existe deja en staging.
|
|
* - INSERT si nouveau cell_id sur un eNB existant ou nouveau.
|
|
* - Rien n'est jamais ecrit dans `cells`/`sites` ici.
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/admin_auth.php';
|
|
require_once __DIR__ . '/../lib/db.php';
|
|
require_once __DIR__ . '/_nav.php';
|
|
|
|
require_admin_auth();
|
|
|
|
$db = pdo();
|
|
$user = current_admin_username();
|
|
|
|
$errorMessage = null;
|
|
$successMessage = null;
|
|
$importSummary = null;
|
|
|
|
const MAX_JSON_SIZE_BYTES = 2 * 1024 * 1024;
|
|
|
|
// Bandes 4G LTE courantes (liste 3GPP complete)
|
|
const LTE_BANDS = [
|
|
'B1' => 'B1 (2100 MHz)',
|
|
'B2' => 'B2 (1900 MHz)',
|
|
'B3' => 'B3 (1800 MHz)',
|
|
'B4' => 'B4 (AWS)',
|
|
'B5' => 'B5 (850 MHz)',
|
|
'B7' => 'B7 (2600 MHz)',
|
|
'B8' => 'B8 (900 MHz)',
|
|
'B20' => 'B20 (800 MHz)',
|
|
'B28' => 'B28 (700 MHz)',
|
|
'B38' => 'B38 (TDD 2600)',
|
|
'B40' => 'B40 (TDD 2300)',
|
|
'B41' => 'B41 (TDD 2500)',
|
|
];
|
|
|
|
/**
|
|
* Recupere les mappings bande->EARFCN pour un operateur donne.
|
|
* Retourne un tableau associatif ['B20' => ['start'=>6200,'end'=>6349], ...]
|
|
*/
|
|
function getOperatorBandMappings(PDO $db, int $operatorId): array
|
|
{
|
|
$stmt = $db->prepare('SELECT band, earfcn_start, earfcn_end FROM operator_band_mappings WHERE operator_id = ?');
|
|
$stmt->execute([$operatorId]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$mappings = [];
|
|
foreach ($rows as $row) {
|
|
$mappings[$row['band']] = [
|
|
'start' => (int)$row['earfcn_start'],
|
|
'end' => (int)$row['earfcn_end'],
|
|
];
|
|
}
|
|
return $mappings;
|
|
}
|
|
|
|
/**
|
|
* Deduit l'EARFCN pour une bande donnee selon le mapping operateur.
|
|
* Retourne l'EARFCN de debut de plage (valeur typique utilisee par l'operateur).
|
|
*/
|
|
function getEarfcnForBand(array $mappings, string $band): ?int
|
|
{
|
|
if (!isset($mappings[$band])) {
|
|
return null;
|
|
}
|
|
return $mappings[$band]['start'];
|
|
}
|
|
|
|
/**
|
|
* Convertit un EARFCN LTE en numero de bande 3GPP (fallback si pas de mapping operateur).
|
|
*/
|
|
function earfcnToBand(int $earfcn, string $tech): ?string
|
|
{
|
|
if ($tech !== '4G') {
|
|
return null;
|
|
}
|
|
static $ranges = [
|
|
[0, 599, 'B1'], [600, 1199, 'B2'], [1200, 1949, 'B3'], [1950, 2399, 'B4'],
|
|
[2400, 2649, 'B5'], [2650, 2749, 'B6'], [2750, 3449, 'B7'], [3450, 3799, 'B8'],
|
|
[3800, 4149, 'B9'], [4150, 4749, 'B10'], [4750, 4949, 'B11'], [5010, 5179, 'B12'],
|
|
[5180, 5279, 'B13'], [5280, 5379, 'B14'], [5730, 5849, 'B18'], [5850, 5999, 'B19'],
|
|
[6000, 6149, 'B20'], [6150, 6449, 'B21'], [6450, 6599, 'B22'], [6600, 7399, 'B23'],
|
|
[7500, 7699, 'B24'], [7700, 8039, 'B25'], [8040, 8689, 'B26'], [8690, 9039, 'B27'],
|
|
[9040, 9209, 'B28'], [9210, 9659, 'B29'], [9660, 9769, 'B30'], [9770, 9869, 'B31'],
|
|
[9870, 10359, 'B32'],
|
|
[36000, 36199, 'B33'], [36200, 36349, 'B34'], [36350, 36949, 'B35'],
|
|
[36950, 37549, 'B36'], [37550, 37749, 'B37'], [37750, 38249, 'B38'],
|
|
[38250, 38649, 'B39'], [38650, 39649, 'B40'], [39650, 41589, 'B41'],
|
|
[41590, 43589, 'B42'], [43590, 45589, 'B43'], [45590, 46589, 'B44'],
|
|
[46590, 46789, 'B45'], [46790, 54539, 'B46'], [54540, 55239, 'B47'],
|
|
[55240, 56739, 'B48'], [65536, 66435, 'B65'], [66436, 67335, 'B66'],
|
|
[67336, 67535, 'B67'], [67536, 67835, 'B68'], [67836, 68335, 'B69'],
|
|
[68336, 68585, 'B70'], [68586, 68935, 'B71'], [68936, 68985, 'B72'],
|
|
[68986, 69035, 'B73'], [69036, 69465, 'B74'], [69466, 70315, 'B75'],
|
|
[70316, 70365, 'B76'], [70366, 70545, 'B85'],
|
|
];
|
|
foreach ($ranges as [$lo, $hi, $band]) {
|
|
if ($earfcn >= $lo && $earfcn <= $hi) {
|
|
return $band;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function ratToTech(string $rat): ?string
|
|
{
|
|
$rat = strtoupper(trim($rat));
|
|
return match (true) {
|
|
str_contains($rat, 'NR') => '5G',
|
|
str_contains($rat, 'LTE') => '4G',
|
|
str_contains($rat, 'UMTS'), str_contains($rat, 'HSPA'), str_contains($rat, 'WCDMA') => '3G',
|
|
str_contains($rat, 'GSM'), str_contains($rat, 'EDGE'), str_contains($rat, 'GPRS') => '2G',
|
|
str_contains($rat, 'CDMA') => 'CDMA',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
function parseCellMapperJson(string $json): array
|
|
{
|
|
$decoded = json_decode($json, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return [null, [], 'JSON invalide : ' . json_last_error_msg()];
|
|
}
|
|
|
|
$data = $decoded['responseData'] ?? $decoded;
|
|
if (!is_array($data)) {
|
|
return [null, [], 'Structure JSON inattendue : cle "responseData" absente ou invalide.'];
|
|
}
|
|
|
|
$rat = (string)($data['RAT'] ?? $data['RATSubType'] ?? '');
|
|
$tech = ratToTech($rat);
|
|
if ($tech === null) {
|
|
return [null, [], "Technologie radio non reconnue (RAT=\"$rat\")."];
|
|
}
|
|
|
|
$siteId = $data['siteID'] ?? null;
|
|
if ($siteId === null || !is_numeric($siteId)) {
|
|
return [null, [], 'Champ "siteID" (eNB) absent ou invalide dans le JSON.'];
|
|
}
|
|
$enb = (int)$siteId;
|
|
|
|
$regionId = $data['regionID'] ?? null;
|
|
$areaCode = ($regionId !== null && is_numeric($regionId)) ? (int)$regionId : null;
|
|
|
|
$provider = $data['Provider'] ?? [];
|
|
$providerId = $provider['providerID'] ?? null;
|
|
|
|
$cellsRaw = $data['cells'] ?? [];
|
|
if (!is_array($cellsRaw) || empty($cellsRaw)) {
|
|
return [null, [], 'Aucune cellule trouvee dans le champ "cells" du JSON.'];
|
|
}
|
|
|
|
$channels = $data['channels'] ?? [];
|
|
$channels = is_array($channels) ? array_values($channels) : [];
|
|
|
|
$entries = [];
|
|
foreach ($cellsRaw as $internalId => $cell) {
|
|
if (!is_array($cell) || !isset($cell['Sector'])) {
|
|
continue;
|
|
}
|
|
$entries[] = $cell;
|
|
}
|
|
usort($entries, static fn($a, $b) => ((int)$a['Sector']) <=> ((int)$b['Sector']));
|
|
|
|
$meta = [
|
|
'tech' => $tech,
|
|
'enb' => $enb,
|
|
'area_code' => $areaCode,
|
|
'provider_id' => $providerId !== null ? (int)$providerId : null,
|
|
'site_id_raw' => (string)$siteId,
|
|
];
|
|
|
|
$cells = [];
|
|
foreach ($entries as $index => $cell) {
|
|
$cellId = (int)$cell['Sector'];
|
|
$pci = isset($cell['PCI']) && is_numeric($cell['PCI']) ? (int)$cell['PCI'] : null;
|
|
|
|
$earfcn = null;
|
|
if (isset($channels[$index]) && is_numeric($channels[$index])) {
|
|
$earfcn = (int)$channels[$index];
|
|
}
|
|
|
|
$bandwidth = null;
|
|
if (isset($cell['Bandwidth']) && is_numeric($cell['Bandwidth'])) {
|
|
$bandwidth = ((int)$cell['Bandwidth']) . 'MHz';
|
|
}
|
|
|
|
$band = $earfcn !== null ? earfcnToBand($earfcn, $tech) : null;
|
|
|
|
$cells[] = [
|
|
'cell_id' => $cellId,
|
|
'pci' => $pci,
|
|
'arfcn' => $earfcn,
|
|
'band' => $band,
|
|
'bandwidth' => $bandwidth,
|
|
];
|
|
}
|
|
|
|
return [$meta, $cells, null];
|
|
}
|
|
|
|
function findStagingCellsForEnb(PDO $db, string $tech, string $mcc, string $mnc, int $enb): array
|
|
{
|
|
$stmt = $db->prepare(
|
|
'SELECT id, cell_id, sub_id, arfcn, band, bandwidth, area_code, status
|
|
FROM staging_cells
|
|
WHERE tech = ? AND mcc = ? AND mnc = ? AND sub_id2 = ?'
|
|
);
|
|
$stmt->execute([$tech, $mcc, $mnc, $enb]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$byCellId = [];
|
|
foreach ($rows as $row) {
|
|
$byCellId[(int)$row['cell_id']] = $row;
|
|
}
|
|
return $byCellId;
|
|
}
|
|
|
|
// Traitement du formulaire
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
require_csrf_token();
|
|
|
|
$mcc = trim((string)($_POST['mcc'] ?? ''));
|
|
$mnc = trim((string)($_POST['mnc'] ?? ''));
|
|
$operatorId = (int)($_POST['operator_id'] ?? 0);
|
|
$selectedBand = trim((string)($_POST['band'] ?? ''));
|
|
$jsonInput = trim((string)($_POST['json_input'] ?? ''));
|
|
$uploadedFile = $_FILES['json_file'] ?? null;
|
|
|
|
if ($uploadedFile && $uploadedFile['error'] === UPLOAD_ERR_OK) {
|
|
if (!is_uploaded_file($uploadedFile['tmp_name'])) {
|
|
$errorMessage = 'Fichier invalide (upload non reconnu comme legitime).';
|
|
} elseif ($uploadedFile['size'] > MAX_JSON_SIZE_BYTES) {
|
|
$errorMessage = 'Le fichier depasse la limite de ' . (MAX_JSON_SIZE_BYTES / 1024 / 1024) . ' Mo.';
|
|
} else {
|
|
$jsonInput = (string)file_get_contents($uploadedFile['tmp_name']);
|
|
}
|
|
}
|
|
|
|
if ($errorMessage === null && $jsonInput === '') {
|
|
$errorMessage = 'Aucune donnee JSON fournie (ni fichier, ni texte colle).';
|
|
} elseif ($errorMessage === null && !preg_match('/^\d{3}$/', $mcc)) {
|
|
$errorMessage = 'MCC invalide (3 chiffres attendus).';
|
|
} elseif ($errorMessage === null && !preg_match('/^\d{2,3}$/', $mnc)) {
|
|
$errorMessage = 'MNC invalide (2 ou 3 chiffres attendus).';
|
|
} elseif ($errorMessage === null && strlen($jsonInput) > MAX_JSON_SIZE_BYTES) {
|
|
$errorMessage = 'Le contenu JSON colle depasse la limite de ' . (MAX_JSON_SIZE_BYTES / 1024 / 1024) . ' Mo.';
|
|
} else {
|
|
$mcc = str_pad($mcc, 3, '0', STR_PAD_LEFT);
|
|
$mnc = str_pad($mnc, 2, '0', STR_PAD_LEFT);
|
|
|
|
[$meta, $parsedCells, $parseError] = parseCellMapperJson($jsonInput);
|
|
|
|
if ($parseError !== null) {
|
|
$errorMessage = $parseError;
|
|
} else {
|
|
$tech = $meta['tech'];
|
|
$enb = $meta['enb'];
|
|
$areaCode = $meta['area_code'];
|
|
|
|
try {
|
|
// Si operator_id est fourni, recuperer ses mappings bande/EARFCN
|
|
$bandMappings = $operatorId > 0 ? getOperatorBandMappings($db, $operatorId) : [];
|
|
|
|
// Si une bande est selectionnee, deduire l'EARFCN du mapping operateur
|
|
$forcedEarfcn = null;
|
|
$forcedBand = null;
|
|
if ($selectedBand !== '' && !empty($bandMappings[$selectedBand])) {
|
|
$forcedBand = strtoupper($selectedBand);
|
|
$forcedEarfcn = getEarfcnForBand($bandMappings, $forcedBand);
|
|
}
|
|
|
|
$stmt = $db->prepare('SELECT id FROM operators WHERE mcc = ? AND mnc = ?');
|
|
$stmt->execute([$mcc, $mnc]);
|
|
$operatorIdFromDb = $stmt->fetchColumn();
|
|
$operatorIdFromDb = $operatorIdFromDb !== false ? (int)$operatorIdFromDb : null;
|
|
|
|
$existingByCellId = findStagingCellsForEnb($db, $tech, $mcc, $mnc, $enb);
|
|
|
|
$updateStmt = $db->prepare(
|
|
'UPDATE staging_cells
|
|
SET sub_id = ?, arfcn = ?, band = ?, bandwidth = ?, area_code = COALESCE(area_code, ?)
|
|
WHERE id = ?'
|
|
);
|
|
$insertStmt = $db->prepare(
|
|
'INSERT INTO staging_cells
|
|
(import_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, raw_line, source, possible_dup_of, status)
|
|
VALUES
|
|
(NULL, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
?, NULL, NULL, ?, NULL,
|
|
NULL, NULL, ?, ?, ?, "cellmapper_import", NULL, "pending")'
|
|
);
|
|
|
|
$db->beginTransaction();
|
|
|
|
$updated = 0;
|
|
$created = 0;
|
|
$skippedNoChange = 0;
|
|
$details = [];
|
|
|
|
foreach ($parsedCells as $cell) {
|
|
$cellId = $cell['cell_id'];
|
|
|
|
// Appliquer la bande et l'EARFCN forces si selectionnes
|
|
$bandToUse = $forcedBand ?? $cell['band'];
|
|
$arfcnToUse = $forcedEarfcn ?? $cell['arfcn'];
|
|
|
|
if (isset($existingByCellId[$cellId])) {
|
|
$existing = $existingByCellId[$cellId];
|
|
|
|
$noChange = (int)($existing['sub_id'] ?? -1) === ($cell['pci'] ?? -1)
|
|
&& (int)($existing['arfcn'] ?? -1) === ($arfcnToUse ?? -1)
|
|
&& ($existing['band'] ?? null) === $bandToUse
|
|
&& ($existing['bandwidth'] ?? null) === $cell['bandwidth']
|
|
&& ($existing['area_code'] !== null || $areaCode === null);
|
|
|
|
if ($noChange) {
|
|
$skippedNoChange++;
|
|
$details[] = "Cellule $cellId (eNB $enb) : deja a jour, aucune modification.";
|
|
continue;
|
|
}
|
|
|
|
$updateStmt->execute([
|
|
$cell['pci'],
|
|
$arfcnToUse,
|
|
$bandToUse,
|
|
$cell['bandwidth'],
|
|
$areaCode,
|
|
$existing['id'],
|
|
]);
|
|
$updated++;
|
|
$tacNote = $existing['area_code'] === null && $areaCode !== null ? ", TAC=$areaCode (complete)" : '';
|
|
$details[] = "Cellule $cellId (eNB $enb) : mise a jour (PCI={$cell['pci']}, ARFCN=$arfcnToUse, bande=$bandToUse$tacNote).";
|
|
} else {
|
|
$rawLine = sprintf(
|
|
'cellmapper:enb=%d;cell_id=%d;pci=%s;arfcn=%s;band=%s;bandwidth=%s;tac=%s',
|
|
$enb,
|
|
$cellId,
|
|
$cell['pci'] ?? 'null',
|
|
$arfcnToUse ?? 'null',
|
|
$bandToUse ?? 'null',
|
|
$cell['bandwidth'] ?? 'null',
|
|
$areaCode ?? 'null'
|
|
);
|
|
|
|
$insertStmt->execute([
|
|
$tech,
|
|
$mcc,
|
|
$mnc,
|
|
$operatorIdFromDb,
|
|
$cellId,
|
|
$areaCode,
|
|
$cell['pci'],
|
|
$enb,
|
|
$bandToUse,
|
|
$cell['bandwidth'],
|
|
'Import CellMapper : coordonnees/azimut/adresse a completer manuellement',
|
|
$arfcnToUse,
|
|
$rawLine,
|
|
]);
|
|
$created++;
|
|
$isNewEnb = empty($existingByCellId);
|
|
$details[] = "Cellule $cellId (eNB $enb) : creee" . ($isNewEnb ? ' (nouvel eNB, a localiser via cadastre)' : ' (nouvelle cellule sur eNB existant)') . ", TAC=" . ($areaCode ?? 'inconnu') . ".";
|
|
}
|
|
}
|
|
|
|
$db->commit();
|
|
|
|
$importSummary = [
|
|
'tech' => $tech,
|
|
'enb' => $enb,
|
|
'mcc' => $mcc,
|
|
'mnc' => $mnc,
|
|
'area_code' => $areaCode,
|
|
'total_parsed' => count($parsedCells),
|
|
'updated' => $updated,
|
|
'created' => $created,
|
|
'skipped_no_change' => $skippedNoChange,
|
|
'details' => $details,
|
|
];
|
|
|
|
$successMessage = sprintf(
|
|
'eNB %d (%s, MCC %s / MNC %s, TAC %s) : %d cellule(s) analysee(s) — %d creee(s), %d mise(s) a jour, %d deja a jour.',
|
|
$enb,
|
|
$tech,
|
|
$mcc,
|
|
$mnc,
|
|
$areaCode ?? 'inconnu',
|
|
count($parsedCells),
|
|
$created,
|
|
$updated,
|
|
$skippedNoChange
|
|
);
|
|
} catch (Throwable $e) {
|
|
if ($db->inTransaction()) {
|
|
$db->rollBack();
|
|
}
|
|
error_log('[cellmapper_import] ' . $e->getMessage());
|
|
$errorMessage = 'Une erreur est survenue lors du traitement. Aucune donnee n\'a ete modifiee. Consultez les journaux du serveur.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Charger les operateurs pour le select
|
|
$operators = $db->query(
|
|
'SELECT o.*, c.name AS country_name FROM operators o
|
|
JOIN countries c ON c.id = o.country_id
|
|
ORDER BY c.name, o.brand'
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$operatorsByCountry = [];
|
|
foreach ($operators as $op) {
|
|
$operatorsByCountry[$op['country_id']][] = $op;
|
|
}
|
|
|
|
// Charger les mappings pour chaque operateur (pour le JS)
|
|
$bandMappingsByOperator = [];
|
|
$mappings = $db->query('SELECT * FROM operator_band_mappings ORDER BY operator_id, band')->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($mappings as $m) {
|
|
$bandMappingsByOperator[$m['operator_id']][] = $m;
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8"><title>NetMonster Admin - Import CellMapper</title>
|
|
<link rel="stylesheet" href="assets/theme.css">
|
|
<style>
|
|
.upload-zone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 1.5rem; text-align: center; background: var(--surface-alt); }
|
|
.upload-zone input[type="file"] { margin-top: .8rem; }
|
|
.stats-inline { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; }
|
|
.detail-line { font-size: .82rem; color: var(--text-muted); font-family: monospace; padding: .2rem 0; border-bottom: 1px solid var(--border); }
|
|
.detail-line:last-child { border-bottom: none; }
|
|
textarea[name="json_input"] { width: 100%; font-family: monospace; font-size: .82rem; }
|
|
.form-row { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
.form-row label { flex: 1; min-width: 150px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<?php render_admin_nav('cellmapper_import.php', $user); ?>
|
|
|
|
<div class="page-wrap">
|
|
<h1>Import CellMapper (JSON)</h1>
|
|
<p class="page-subtitle">
|
|
Collez ou televersez l'export JSON d'un site CellMapper (un eNB). Selectionnez la bande 4G pour
|
|
remplir automatiquement l'EARFCN selon le mapping de l'operateur. Seuls les parametres radio
|
|
techniques sont extraits (Cell ID, PCI, EARFCN, bande, largeur de bande, TAC).
|
|
</p>
|
|
|
|
<?php if ($successMessage): ?><p class="msg-ok"><?= htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8') ?></p><?php endif; ?>
|
|
<?php if ($errorMessage): ?><p class="msg-err"><?= htmlspecialchars($errorMessage, ENT_QUOTES, 'UTF-8') ?></p><?php endif; ?>
|
|
|
|
<div class="card">
|
|
<h2>Nouvel import</h2>
|
|
<form method="post" enctype="multipart/form-data" id="cellmapper-form">
|
|
<?= csrf_field() ?>
|
|
|
|
<div class="form-row">
|
|
<label>MCC
|
|
<input type="text" name="mcc" pattern="\d{3}" maxlength="3" placeholder="270" required value="<?= htmlspecialchars((string)($_POST['mcc'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
|
</label>
|
|
<label>MNC
|
|
<input type="text" name="mnc" pattern="\d{2,3}" maxlength="3" placeholder="01" required value="<?= htmlspecialchars((string)($_POST['mnc'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
|
</label>
|
|
<label>Operateur (optionnel, pour mapping EARFCN)
|
|
<select name="operator_id" id="operator-select">
|
|
<option value="">-- selectionner --</option>
|
|
<?php foreach ($operators as $op): ?>
|
|
<option value="<?= (int)$op['id'] ?>" data-mcc="<?= htmlspecialchars($op['mcc'], ENT_QUOTES, 'UTF-8') ?>" data-mnc="<?= htmlspecialchars($op['mnc'], ENT_QUOTES, 'UTF-8') ?>">
|
|
<?= htmlspecialchars($op['brand'], ENT_QUOTES, 'UTF-8') ?> (<?= htmlspecialchars($op['country_name'], ENT_QUOTES, 'UTF-8') ?>)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label>Bande 4G (optionnel, remplit EARFCN auto)
|
|
<select name="band" id="band-select">
|
|
<option value="">-- deduire du JSON ou laisser vide --</option>
|
|
<?php foreach (LTE_BANDS as $bandCode => $bandLabel): ?>
|
|
<option value="<?= htmlspecialchars($bandCode, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($bandLabel, ENT_QUOTES, 'UTF-8') ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
<label>EARFCN (rempli auto si bande + operateur selectionnes)
|
|
<input type="number" name="earfcn_auto" id="earfcn-auto" readonly placeholder="Automatique">
|
|
<input type="hidden" name="earfcn_forced" id="earfcn-forced">
|
|
</label>
|
|
</div>
|
|
|
|
<p class="page-subtitle" style="margin-top:-.5rem;">
|
|
MCC/MNC ne sont pas presents dans le JSON CellMapper : ils doivent etre saisis manuellement.
|
|
Si vous selectionnez un operateur et une bande, l'EARFCN sera rempli automatiquement selon
|
|
le mapping configure dans <code>countries_settings.php</code>.
|
|
</p>
|
|
|
|
<div class="upload-zone">
|
|
<p>Selectionnez un fichier <strong>.json</strong> (taille maximale : 2 Mo)</p>
|
|
<input type="file" name="json_file" accept=".json,application/json">
|
|
</div>
|
|
|
|
<label style="display:block;margin-top:1rem;">ou collez le JSON ici
|
|
<textarea name="json_input" rows="12" placeholder='{ "responseData": { "RAT": "LTE", "siteID": "4292", "regionID": "12", "cells": {...}, "channels": [...] } }'><?= htmlspecialchars((string)($_POST['json_input'] ?? ''), ENT_QUOTES, 'UTF-8') ?></textarea>
|
|
</label>
|
|
|
|
<button type="submit" class="primary" style="margin-top:1rem;">Analyser et integrer en staging</button>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if ($importSummary): ?>
|
|
<div class="card">
|
|
<h2>Resultat de l'import — eNB <?= (int)$importSummary['enb'] ?> (<?= htmlspecialchars($importSummary['tech'], ENT_QUOTES, 'UTF-8') ?>) — TAC <?= htmlspecialchars((string)($importSummary['area_code'] ?? 'inconnu'), ENT_QUOTES, 'UTF-8') ?></h2>
|
|
<div class="stats-inline">
|
|
<div class="stat-chip"><strong><?= (int)$importSummary['total_parsed'] ?></strong>cellule(s) analysee(s)</div>
|
|
<div class="stat-chip"><strong><?= (int)$importSummary['created'] ?></strong>creee(s)</div>
|
|
<div class="stat-chip"><strong><?= (int)$importSummary['updated'] ?></strong>mise(s) a jour</div>
|
|
<div class="stat-chip"><strong><?= (int)$importSummary['skipped_no_change'] ?></strong>deja a jour (ignoree(s))</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top:1.2rem;">Detail</h3>
|
|
<?php foreach ($importSummary['details'] as $line): ?>
|
|
<div class="detail-line"><?= htmlspecialchars($line, ENT_QUOTES, 'UTF-8') ?></div>
|
|
<?php endforeach; ?>
|
|
|
|
<p style="margin-top:1rem;"><a href="review.php" class="btn primary" style="text-decoration:none;display:inline-block;">Aller a la page de validation →</a></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card">
|
|
<h2>Rappel des regles appliquees</h2>
|
|
<ul>
|
|
<li>Correspondance stricte sur <code>(tech, mcc, mnc, eNB)</code> : jamais de melange entre operateurs differents.</li>
|
|
<li>Le TAC/area_code provient de <code>regionID</code> et n'est utilise que pour completer les lignes ou il est absent.</li>
|
|
<li>Cellule deja en staging avec le meme Cell ID → mise a jour des seuls champs radio (PCI, EARFCN, bande, largeur de bande, TAC si manquant).</li>
|
|
<li>Cellule non presente sur un eNB deja en staging → ajout.</li>
|
|
<li>eNB totalement absent du staging → ajout quand meme, a localiser ensuite via le cadastre.</li>
|
|
<li>Coordonnees, azimut et adresse ne sont jamais lus depuis CellMapper ni ecrases par cet import.</li>
|
|
<li>Si une bande est selectionnee, l'EARFCN est deduit du mapping operateur (sinon, deduction depuis le JSON).</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Mappings bande/EARFCN par operateur (injectes depuis PHP)
|
|
const bandMappingsByOperator = <?= json_encode($bandMappingsByOperator, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
const operatorSelect = document.getElementById('operator-select');
|
|
const bandSelect = document.getElementById('band-select');
|
|
const earfcnAutoInput = document.getElementById('earfcn-auto');
|
|
const earfcnForcedInput = document.getElementById('earfcn-forced');
|
|
|
|
// Mettre a jour MCC/MNC quand un operateur est selectionne
|
|
operatorSelect.addEventListener('change', function() {
|
|
const selectedOption = this.options[this.selectedIndex];
|
|
const mcc = selectedOption.dataset.mcc || '';
|
|
const mnc = selectedOption.dataset.mnc || '';
|
|
if (mcc) document.querySelector('input[name="mcc"]').value = mcc;
|
|
if (mnc) document.querySelector('input[name="mnc"]').value = mnc;
|
|
|
|
// Re-calculer EARFCN si bande deja selectionnee
|
|
if (bandSelect.value) {
|
|
updateEarfcnFromBand();
|
|
}
|
|
});
|
|
|
|
// Mettre a jour EARFCN quand une bande est selectionnee
|
|
bandSelect.addEventListener('change', updateEarfcnFromBand);
|
|
|
|
function updateEarfcnFromBand() {
|
|
const operatorId = operatorSelect.value;
|
|
const band = bandSelect.value;
|
|
|
|
if (!operatorId || !band) {
|
|
earfcnAutoInput.value = '';
|
|
earfcnForcedInput.value = '';
|
|
return;
|
|
}
|
|
|
|
const mappings = bandMappingsByOperator[operatorId] || [];
|
|
const mapping = mappings.find(m => m.band === band);
|
|
|
|
if (mapping) {
|
|
// Utiliser le debut de plage comme EARFCN typique
|
|
earfcnAutoInput.value = mapping.earfcn_start;
|
|
earfcnForcedInput.value = mapping.earfcn_start;
|
|
} else {
|
|
earfcnAutoInput.value = 'Aucun mapping pour ' + band;
|
|
earfcnForcedInput.value = '';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|