Files
admin-netmonster-db/admin/cellmapper_import.php
T
2026-10-21 22:00:06 +02:00

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 &rarr;</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 &rarr; 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 &rarr; ajout.</li>
<li>eNB totalement absent du staging &rarr; 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>