249 lines
12 KiB
PHP
249 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* admin/import_ntm.php
|
|
*
|
|
* Interface d'upload manuel d'un fichier .ntm directement depuis l'admin
|
|
* (en plus du flux existant via contribute.php). Reutilise
|
|
* importNtmFileToStaging() de lib/ntm_import_to_staging.php : le fichier
|
|
* est parse, chaque cellule extraite atterrit dans staging_cells (status
|
|
* pending), rien n'est ecrit dans sites/cells sans validation manuelle
|
|
* via /admin/review.php.
|
|
*
|
|
* Apres import, la page affiche un APERCU du regroupement par site
|
|
* (meme logique deriveSiteKey() que review.php) pour confirmer
|
|
* immediatement que les cellules d'un meme site sont bien reunies
|
|
* ensemble, avant meme d'aller sur la page de validation.
|
|
*
|
|
* Tous les fichiers numeriques passes a htmlspecialchars() sont castes
|
|
* en (string) au prealable (voir correctifs precedents sur review.php
|
|
* et edit_site.php pour PHP 8.3+).
|
|
*
|
|
* Corrige suite a l'audit du 20/09/2026 : le detail technique d'une
|
|
* exception ($e->getMessage()) n'est plus affiche a l'ecran ; il est
|
|
* journalise via error_log() et un message generique est montre a
|
|
* l'administrateur.
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/admin_auth.php';
|
|
require_once __DIR__ . '/../lib/db.php';
|
|
require_once __DIR__ . '/../lib/ntm_import_to_staging.php';
|
|
require_once __DIR__ . '/_nav.php';
|
|
|
|
require_admin_auth();
|
|
$db = pdo();
|
|
$user = current_admin_username();
|
|
|
|
$successMessage = null;
|
|
$errorMessage = null;
|
|
$importStats = null;
|
|
$previewGroups = [];
|
|
$lastImportId = null;
|
|
|
|
/**
|
|
* Meme logique de regroupement que admin/review.php, dupliquee ici
|
|
* volontairement pour ne pas creer de dependance croisee entre pages
|
|
* admin. Si vous ajustez le decalage de bits ou la logique de
|
|
* regroupement dans review.php, reportez le changement ici aussi.
|
|
*/
|
|
function deriveSiteKeyForPreview(array $row): string
|
|
{
|
|
$mcc = $row['mcc'];
|
|
$mnc = $row['mnc'];
|
|
|
|
if ($row['tech'] === '4G' && !empty($row['sub_id2'])) {
|
|
return "enb:{$mcc}:{$mnc}:{$row['area_code']}:{$row['sub_id2']}";
|
|
}
|
|
|
|
$latR = $row['lat'] !== null ? round((float)$row['lat'], 4) : 'na';
|
|
$lonR = $row['lon'] !== null ? round((float)$row['lon'], 4) : 'na';
|
|
return "geo:{$mcc}:{$mnc}:{$row['area_code']}:{$latR}:{$lonR}";
|
|
}
|
|
|
|
const MAX_UPLOAD_SIZE_BYTES = 10 * 1024 * 1024; // 10 Mo, garde-fou raisonnable pour un fichier .ntm
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
require_csrf_token();
|
|
|
|
$countryId = (int)($_POST['country_id'] ?? 0);
|
|
|
|
if (!$countryId) {
|
|
$errorMessage = "Selectionnez un pays avant d'importer.";
|
|
} elseif (empty($_FILES['ntm_file']) || $_FILES['ntm_file']['error'] !== UPLOAD_ERR_OK) {
|
|
$uploadErrors = [
|
|
UPLOAD_ERR_INI_SIZE => "Le fichier depasse la taille maximale autorisee par le serveur (php.ini).",
|
|
UPLOAD_ERR_FORM_SIZE => "Le fichier depasse la taille maximale autorisee par le formulaire.",
|
|
UPLOAD_ERR_PARTIAL => "Le fichier n'a ete que partiellement televerse. Reessayez.",
|
|
UPLOAD_ERR_NO_FILE => "Aucun fichier selectionne.",
|
|
];
|
|
$code = $_FILES['ntm_file']['error'] ?? UPLOAD_ERR_NO_FILE;
|
|
$errorMessage = $uploadErrors[$code] ?? "Erreur lors du televersement du fichier.";
|
|
} else {
|
|
$tmpPath = $_FILES['ntm_file']['tmp_name'];
|
|
$originalName = $_FILES['ntm_file']['name'];
|
|
$fileSize = $_FILES['ntm_file']['size'];
|
|
|
|
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
|
|
|
if ($extension !== 'ntm') {
|
|
$errorMessage = "Seuls les fichiers .ntm sont acceptes (extension detectee : ." . htmlspecialchars($extension, ENT_QUOTES, 'UTF-8') . ").";
|
|
} elseif ($fileSize > MAX_UPLOAD_SIZE_BYTES) {
|
|
$errorMessage = "Le fichier depasse la limite de " . (MAX_UPLOAD_SIZE_BYTES / 1024 / 1024) . " Mo.";
|
|
} elseif (!is_uploaded_file($tmpPath)) {
|
|
$errorMessage = "Televersement invalide (fichier non reconnu comme upload legitime).";
|
|
} else {
|
|
$safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', $originalName);
|
|
$uploadsDir = __DIR__ . '/../uploads';
|
|
if (!is_dir($uploadsDir)) {
|
|
mkdir($uploadsDir, 0750, true);
|
|
}
|
|
$destPath = $uploadsDir . '/' . uniqid('ntm_', true) . '_' . $safeFilename;
|
|
|
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
|
$errorMessage = "Impossible d'enregistrer le fichier televerse sur le serveur.";
|
|
} else {
|
|
try {
|
|
$importStats = importNtmFileToStaging($destPath, $safeFilename, $countryId);
|
|
$successMessage = "Import termine : {$importStats['staged']} ligne(s) placee(s) en validation"
|
|
. ($importStats['already_known'] > 0 ? ", dont {$importStats['already_known']} deja connue(s) en base" : "")
|
|
. ($importStats['error'] > 0 ? ", {$importStats['error']} erreur(s)" : "") . ".";
|
|
|
|
// Recupere l'ID de l'import qui vient d'etre cree pour l'apercu de regroupement
|
|
$lastImportId = (int)$db->query('SELECT MAX(id) FROM imports')->fetchColumn();
|
|
|
|
$previewStmt = $db->prepare('
|
|
SELECT sc.*, o.brand AS operator_brand
|
|
FROM staging_cells sc
|
|
LEFT JOIN operators o ON o.id = sc.operator_guess
|
|
WHERE sc.import_id = ?
|
|
');
|
|
$previewStmt->execute([$lastImportId]);
|
|
$importedRows = $previewStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($importedRows as $row) {
|
|
$key = deriveSiteKeyForPreview($row);
|
|
$previewGroups[$key]['cells'][] = $row;
|
|
$previewGroups[$key]['operator_brand'] = $row['operator_brand'];
|
|
$previewGroups[$key]['lat'] = $previewGroups[$key]['lat'] ?? $row['lat'];
|
|
$previewGroups[$key]['lon'] = $previewGroups[$key]['lon'] ?? $row['lon'];
|
|
}
|
|
} catch (Throwable $e) {
|
|
error_log('[import_ntm] ' . $e->getMessage());
|
|
$errorMessage = "Une erreur est survenue lors du traitement du fichier. Consultez les journaux du serveur.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$countries = $db->query('SELECT id, iso2, name FROM countries ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$importHistory = $db->query('
|
|
SELECT i.*, c.name AS country_name
|
|
FROM imports i
|
|
LEFT JOIN countries c ON c.id = i.country_id
|
|
ORDER BY i.uploaded_at DESC
|
|
LIMIT 20
|
|
')->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8"><title>NetMonster Admin - Import .ntm</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; }
|
|
.preview-group { background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .8rem 1rem; margin-bottom: .7rem; }
|
|
.preview-group h4 { margin: 0 0 .4rem; font-size: .88rem; }
|
|
.preview-group .cell-line { font-size: .78rem; color: var(--text-muted); font-family: monospace; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<?php render_admin_nav('import_ntm.php', $user); ?>
|
|
|
|
<div class="page-wrap">
|
|
<h1>Importer un fichier .ntm</h1>
|
|
<p class="page-subtitle">Deposez un fichier .ntm pour l'analyser et placer ses antennes en file de validation. Rien n'est ecrit dans la base finale sans validation manuelle.</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>Nouveau depot</h2>
|
|
<form method="post" enctype="multipart/form-data">
|
|
<?= csrf_field() ?>
|
|
<div class="form-grid" style="margin-bottom:1rem;">
|
|
<div>
|
|
<label>Pays concerne</label>
|
|
<select name="country_id" required>
|
|
<option value="">-- choisir --</option>
|
|
<?php foreach ($countries as $c): ?>
|
|
<option value="<?= (int)$c['id'] ?>"><?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="upload-zone">
|
|
<p>Selectionnez un fichier <strong>.ntm</strong> (taille maximale : 10 Mo)</p>
|
|
<input type="file" name="ntm_file" accept=".ntm" required>
|
|
</div>
|
|
<button type="submit" class="primary" style="margin-top:1rem;">Importer et envoyer en validation</button>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if ($importStats): ?>
|
|
<div class="card">
|
|
<h2>Resultat de l'import</h2>
|
|
<div class="stats-inline">
|
|
<div class="stat-chip"><strong><?= (int)$importStats['total'] ?></strong>ligne(s) lues</div>
|
|
<div class="stat-chip"><strong><?= (int)$importStats['staged'] ?></strong>placee(s) en validation</div>
|
|
<div class="stat-chip"><strong><?= (int)$importStats['already_known'] ?></strong>deja connue(s) (doublon potentiel)</div>
|
|
<div class="stat-chip"><strong><?= (int)$importStats['error'] ?></strong>erreur(s)</div>
|
|
</div>
|
|
|
|
<h2 style="margin-top:1.4rem;">Apercu du regroupement par site (<?= count($previewGroups) ?> site(s) detecte(s))</h2>
|
|
<p class="page-subtitle">Verification rapide que les cellules d'un meme site (meme eNB pour la 4G, ou meme position GPS pour les autres technos) sont bien reunies ensemble.</p>
|
|
|
|
<?php foreach ($previewGroups as $group): ?>
|
|
<div class="preview-group">
|
|
<h4><?= htmlspecialchars($group['operator_brand'] ?? 'Operateur inconnu', ENT_QUOTES, 'UTF-8') ?> · <?= htmlspecialchars((string)$group['lat'], ENT_QUOTES, 'UTF-8') ?>, <?= htmlspecialchars((string)$group['lon'], ENT_QUOTES, 'UTF-8') ?> · <?= count($group['cells']) ?> cellule<?= count($group['cells']) > 1 ? 's' : '' ?></h4>
|
|
<?php foreach ($group['cells'] as $c): ?>
|
|
<div class="cell-line">
|
|
<?= htmlspecialchars((string)$c['tech'], ENT_QUOTES, 'UTF-8') ?> ·
|
|
Cell ID <?= htmlspecialchars((string)$c['cell_id'], ENT_QUOTES, 'UTF-8') ?> ·
|
|
Area code <?= htmlspecialchars((string)$c['area_code'], ENT_QUOTES, 'UTF-8') ?>
|
|
<?php if (!empty($c['sub_id2'])): ?> · eNB/RNC <?= htmlspecialchars((string)$c['sub_id2'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
|
<?php if (!empty($c['band'])): ?> · <?= htmlspecialchars((string)$c['band'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
|
<?php if ($c['possible_dup_of']): ?> · <span class="badge badge-dup">Deja en base #<?= (int)$c['possible_dup_of'] ?></span><?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</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>Historique des imports</h2>
|
|
<table class="data-table">
|
|
<tr><th>Fichier</th><th>Pays</th><th>Date</th><th>Lignes</th><th>En validation</th><th>Erreurs</th><th>Statut</th></tr>
|
|
<?php foreach ($importHistory as $imp): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($imp['filename'], ENT_QUOTES, 'UTF-8') ?></td>
|
|
<td><?= htmlspecialchars($imp['country_name'] ?? '?', ENT_QUOTES, 'UTF-8') ?></td>
|
|
<td><?= htmlspecialchars(date('d/m/Y H:i', strtotime($imp['uploaded_at'])), ENT_QUOTES, 'UTF-8') ?></td>
|
|
<td><?= (int)$imp['total_lines'] ?></td>
|
|
<td><?= (int)$imp['staged_count'] ?></td>
|
|
<td><?= (int)$imp['error_count'] ?></td>
|
|
<td><?= htmlspecialchars($imp['status'], ENT_QUOTES, 'UTF-8') ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($importHistory)): ?><tr><td colspan="7" class="no-cadastre">Aucun import pour le moment.</td></tr><?php endif; ?>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|