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

226 lines
8.7 KiB
PHP

<?php
/**
* admin/import_history.php
*
* Historique des imports .ntm et de leurs erreurs. Lecture seule sauf
* pour la purge optionnelle des lignes d'erreur d'un import, action
* protegee par CSRF + confirmation explicite.
*
* CORRIGE (20/09/2026, suite a erreur 500 en usage reel) :
* - require_once _nav.php manquait, provoquant une erreur fatale
* "Call to undefined function render_admin_nav()".
* - Les tables import_batches / import_errors necessaires a cette
* page n'existaient pas : voir
* admin/sql/migration_add_missing_tables.sql.
*
* IMPORTANT : import_batches/import_errors sont DISTINCTES de la table
* `imports` deja existante et alimentee par lib/ntm_import_to_staging.php
* (utilisee par admin/import_ntm.php). Tant qu'aucune page n'insere
* explicitement dans import_batches/import_errors, cette page
* s'affichera correctement mais restera VIDE ("Aucun import pour le
* moment"), meme si vous avez deja importe des fichiers .ntm via
* import_ntm.php. Une evolution future pourrait brancher cette page
* directement sur la table `imports` existante plutot que de dupliquer
* un systeme de suivi parallele jamais alimente.
*
* Corrige suite a l'audit du 20/09/2026 : le detail technique des
* exceptions n'est plus affiche a l'ecran ; il est journalise via
* error_log().
*
* Hypothese de schema :
* import_batches(id, filename, imported_at, imported_by, status,
* rows_total, rows_imported, rows_rejected, country_id)
* import_errors(id, batch_id, line_number, message, raw_line)
*/
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();
$feedback = null;
$feedbackType = 'success';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf_token();
$action = $_POST['action'] ?? '';
$confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
$batchId = filter_input(INPUT_POST, 'batch_id', FILTER_VALIDATE_INT);
if ($action === 'purge_errors') {
if (!$confirmed || !$batchId) {
$feedback = 'Purge refusee : confirmation ou identifiant manquant.';
$feedbackType = 'error';
} else {
try {
$stmt = $db->prepare('DELETE FROM import_errors WHERE batch_id = ?');
$stmt->execute([$batchId]);
$feedback = sprintf('%d ligne(s) d\'erreur purgee(s) pour l\'import #%d.', $stmt->rowCount(), $batchId);
} catch (Throwable $e) {
error_log('[import_history] purge_errors: ' . $e->getMessage());
$feedback = 'Une erreur est survenue lors de la purge.';
$feedbackType = 'error';
}
}
}
}
$page = max(1, filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1);
$perPage = 25;
$offset = ($page - 1) * $perPage;
$totalBatches = 0;
$batches = [];
try {
$totalBatches = (int)$db->query('SELECT COUNT(*) FROM import_batches')->fetchColumn();
$stmt = $db->prepare(
'SELECT id, filename, imported_at, imported_by, status, rows_total, rows_imported, rows_rejected
FROM import_batches
ORDER BY imported_at DESC
LIMIT :limit OFFSET :offset'
);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$batches = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
error_log('[import_history] liste batches: ' . $e->getMessage());
$feedback = 'Impossible de charger l\'historique des imports. Consultez les journaux du serveur.';
$feedbackType = 'error';
}
$errorsByBatch = [];
$detailBatchId = filter_input(INPUT_GET, 'detail', FILTER_VALIDATE_INT);
if ($detailBatchId) {
try {
$stmt = $db->prepare(
'SELECT line_number, message, raw_line
FROM import_errors
WHERE batch_id = ?
ORDER BY line_number ASC
LIMIT 500'
);
$stmt->execute([$detailBatchId]);
$errorsByBatch = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
error_log('[import_history] detail erreurs: ' . $e->getMessage());
}
}
$totalPages = max(1, (int)ceil($totalBatches / $perPage));
$user = current_admin_username();
function status_badge(string $status): string
{
$map = [
'success' => 'badge-success',
'partial' => 'badge-warning',
'failed' => 'badge-error',
];
$class = $map[$status] ?? 'badge';
return '<span class="badge ' . $class . '">' . htmlspecialchars($status, ENT_QUOTES, 'UTF-8') . '</span>';
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Historique des imports — NetMonster Admin</title>
<link rel="stylesheet" href="assets/theme.css">
</head>
<body>
<?php render_admin_nav('import_history.php', $user); ?>
<main class="page-wrap">
<h1>Historique des imports .ntm</h1>
<p class="page-subtitle">Suivi en lecture seule des imports realises. La purge des erreurs d'un import est possible mais n'affecte jamais les donnees deja validees.</p>
<?php if ($feedback !== null): ?>
<p class="<?= $feedbackType === 'error' ? 'msg-err' : 'msg-ok' ?>">
<?= htmlspecialchars($feedback, ENT_QUOTES, 'UTF-8') ?>
</p>
<?php endif; ?>
<section class="card">
<h2>Imports (<?= $totalBatches ?>)</h2>
<table class="data-table">
<tr>
<th>#</th>
<th>Fichier</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Statut</th>
<th>Lignes totales</th>
<th>Importees</th>
<th>Rejetees</th>
<th>Actions</th>
</tr>
<?php foreach ($batches as $batch): ?>
<tr>
<td><?= (int)$batch['id'] ?></td>
<td><?= htmlspecialchars((string)$batch['filename'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars((string)$batch['imported_at'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars((string)$batch['imported_by'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= status_badge((string)$batch['status']) ?></td>
<td><?= (int)$batch['rows_total'] ?></td>
<td><?= (int)$batch['rows_imported'] ?></td>
<td><?= (int)$batch['rows_rejected'] ?></td>
<td>
<a href="import_history.php?detail=<?= (int)$batch['id'] ?>">Voir erreurs</a>
<?php if ((int)$batch['rows_rejected'] > 0): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Confirmer la purge des erreurs de cet import ?');">
<?= csrf_field() ?>
<input type="hidden" name="action" value="purge_errors">
<input type="hidden" name="confirm" value="1">
<input type="hidden" name="batch_id" value="<?= (int)$batch['id'] ?>">
<button type="submit" class="reject-small">Purger erreurs</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($batches)): ?>
<tr><td colspan="9" class="no-cadastre">Aucun import pour le moment.</td></tr>
<?php endif; ?>
</table>
<?php if ($totalPages > 1): ?>
<nav class="pagination">
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<a href="import_history.php?page=<?= $p ?>" class="<?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor; ?>
</nav>
<?php endif; ?>
</section>
<?php if ($detailBatchId): ?>
<section class="card">
<h2>Erreurs de l'import #<?= (int)$detailBatchId ?></h2>
<?php if (empty($errorsByBatch)): ?>
<p>Aucune erreur enregistree pour cet import.</p>
<?php else: ?>
<table class="data-table">
<tr>
<th>Ligne</th>
<th>Message</th>
<th>Contenu brut</th>
</tr>
<?php foreach ($errorsByBatch as $err): ?>
<tr>
<td><?= (int)$err['line_number'] ?></td>
<td><?= htmlspecialchars((string)$err['message'], ENT_QUOTES, 'UTF-8') ?></td>
<td><code><?= htmlspecialchars((string)$err['raw_line'], ENT_QUOTES, 'UTF-8') ?></code></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</section>
<?php endif; ?>
</main>
</body>
</html>