Aller au contenu

Interface Administration

⬇️ Télécharger cette page en Markdown


Structure du Menu

Le menu latéral est organisé en sections thématiques :

📊 Tableau de bord

🚨 SIGNALEMENTS
   ├── Incidents
   ├── Infos officielles
   ├── Groupes
   ├── Alertes
   ├── Autorisations Audio
   └── Statistiques

👥 Usagers

🔑 Sécurité

🏢 MAIRIE
   ├── Services
   ├── Utilisateurs
   ├── Agents
   ├── Contacts
   └── Planning

⚙️ ADMINISTRATION (admin uniquement)
   ├── Types d'incidents
   ├── Ma Mairie
   └── Exports API

🛡️ SUPER ADMIN (superadmin uniquement)
   └── Emails Invalides

Pages Principales

Page Fichier Section Description
Dashboard index.php - Vue d'ensemble statistiques
Incidents incidents.php Signalements Liste et filtrage des signalements
Détail incident incident_detail.php Signalements Vue détaillée et actions
Infos officielles infos.php Signalements Publications officielles
Groupes groupes.php Signalements Signalements regroupés
Statistiques statistiques.php Signalements Statistiques temporelles + export PDF
Usagers citoyens.php - Gestion des citoyens
Sécurité securite.php - Clés de sécurité WebAuthn (self-service, tout utilisateur)
Services services.php Mairie Départements municipaux
Utilisateurs users.php Mairie Gestion agents backoffice
Agents agents.php Mairie Agents terrain (mobile)
Contacts contacts.php Mairie Contacts de notification
Planning planning.php Mairie Calendrier interventions
Types types.php Administration Configuration catégories
Ma Mairie mairie.php Administration Paramètres de la mairie
Exports API exports.php Administration Clés API et exports

incidents.php — Liste, Kanban, SLA, Vues sauvegardées

Vue Liste / Kanban (v3.1, 2026-08-25)

Switch dans l'en-tête de la section « Liste des incidents », mémorisé par utilisateur (users.incidents_view_pref). Changer de vue via ?vue=liste|kanban persiste automatiquement la préférence :

$storedView = $db->fetchOne("SELECT incidents_view_pref FROM users WHERE id = ?", [$userId])['incidents_view_pref'] ?? 'liste';
$vueParam = $_GET['vue'] ?? null;
if ($vueParam === 'liste' || $vueParam === 'kanban') {
    $currentView = $vueParam;
    if ($currentView !== $storedView) {
        $db->execute("UPDATE users SET incidents_view_pref = ? WHERE id = ?", [$currentView, $userId]);
    }
} else {
    $currentView = $storedView;
}

Le Kanban affiche 4 colonnes alignées sur l'enum réel de incidents.statut — nouveau/en_cours/resolu/ferme, pas rejete (utilisé par endroits ailleurs dans le code mais absent du schéma actuel). Le filtre « Statut » est masqué en vue Kanban ; le changement de statut se fait par drag & drop natif (HTML5 DnD, sans dépendance), qui appelle un endpoint AJAX dans incidents.php :

// POST incidents.php  ajax_action=update_status  incident_id=…  statut=…
// Revérifie mairie + type accessibles avant d'appliquer, journalise dans incident_historique,
// déclenche la notif push citoyen si passage à 'resolu' — même logique que incident_detail.php.

Alertes SLA (v3.1, 2026-08-25)

Badge rouge « SLA DÉPASSÉ » sur les cartes liste et Kanban (bordure rouge + icône côté Kanban) quand le délai configuré est dépassé, et sur incident_detail.php (à côté du badge de statut). Voir Colonnes SLA pour la logique de repli service → type. Configuration : champ « Délai d'alerte SLA (heures) » ajouté aux formulaires de types.php et services.php (vide = pas d'alerte).

Vues sauvegardées (v3.1, 2026-08-25)

Filtres favoris par utilisateur (saved_filters), affichés en pilules au-dessus du formulaire de filtres. Le formulaire « Enregistrer cette vue » n'apparaît que si au moins un filtre est actif :

function currentFilterQueryString() {
    $params = [];
    foreach (['statut', 'type', 'priorite', 'mairie', 'vue'] as $key) {
        if (!empty($_GET[$key])) $params[$key] = $_GET[$key];
    }
    return http_build_query($params);
}

Capture les filtres depuis $_GET — le formulaire POST n'a pas d'attribut action, donc il soumet vers l'URL courante (query string GET incluse), disponible à la fois dans $_GET (filtres à sauvegarder) et $_POST (nom + action) sur la même requête. Chaque vue est scopée à user_id (jamais visible par un autre agent).


incident_detail.php - Détail d'un Incident

Layout 2 colonnes (Desktop-first)

La page utilise une mise en page optimisée pour les agents municipaux sur desktop :

┌──────────────────────────────────────────────────────────────┐
│                     En-tête incident                          │
│  [#ID] Type • Adresse • Statut                               │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────┬────────────────────────────────┐
│      COLONNE GAUCHE         │       COLONNE DROITE           │
│         (55%)               │          (45%)                 │
│                             │                                │
│  📍 Localisation            │  👤 Attribution                │
│  - Carte interactive        │  - Agent assigné               │
│  - Coordonnées GPS          │  - Service                     │
│                             │                                │
│  📝 Signalement             │  ⚙️ Statut & Priorité          │
│  - Description              │  - Changement statut           │
│  - Type (modifiable)        │  - Niveau priorité             │
│  - Photos déclarant         │                                │
│                             │  💬 Commentaires               │
│  👤 Déclarant (accordion)   │  - Historique échanges         │
│  - Identité / Anonyme       │  - Ajout commentaire           │
│  - Contact                  │                                │
│                             │  📅 Planification (accordion)  │
│  📜 Historique (accordion)  │  - Dates intervention          │
│  - Timeline événements      │                                │
│                             │  [Boutons Actions Sticky]      │
│  📸 Photos mairie           │  - Sauvegarder                 │
│  (accordion)                │  - Marquer résolu              │
│                             │  - Rejeter                     │
└─────────────────────────────┴────────────────────────────────┘

Caractéristiques

  • Colonne gauche (55%) : Informations contextuelles en lecture seule
  • Colonne droite (45%) : Actions de l'agent
  • Accordéons : Sections secondaires repliables pour réduire le défilement
  • Boutons sticky : Actions principales toujours visibles en bas de colonne droite
  • Responsive : Empilage vertical sur mobile

Support Modal

La page supporte l'affichage en modal via le paramètre ?modal=1 :

// Détection mode modal
$isModal = isset($_GET['modal']) && $_GET['modal'] == '1';

// Exclusion header/footer en mode modal
if (!$isModal) {
    include __DIR__ . '/src/views/header.php';
}

Usage : Permet d'afficher les détails d'un incident dans une iframe sans la navigation.

Note de Qualité des Signalements

Lors du passage au statut "Résolu", l'agent peut noter la qualité du signalement :

Note Signification
0 Signalement abusif
1 Très insuffisant
2 Insuffisant
3 Correct
4 Bon
5 Excellent

Sauvegarde Asynchrone (AJAX)

La note est sauvegardée immédiatement dès que l'agent sélectionne une valeur, sans attendre la soumission du formulaire.

// Appelé automatiquement au changement de la note
function saveNoteQualite() {
    const formData = new FormData();
    formData.append('ajax_action', 'save_note_qualite');
    formData.append('note_qualite', noteSelect.value);

    fetch(window.location.href, {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        // Feedback: "✓ Note enregistrée"
    });
}

Endpoint PHP :

// Gestion AJAX de la note qualité
if ($_POST['ajax_action'] === 'save_note_qualite') {
    header('Content-Type: application/json');

    $db->execute(
        "UPDATE incidents SET
            note_qualite_signalement = ?,
            note_qualite_date = NOW(),
            note_qualite_user_id = ?
         WHERE id = ?",
        [$noteQualite, $auth->getUser()['id'], $incidentId]
    );

    echo json_encode(['success' => true, 'note' => $noteQualite]);
    exit;
}

UX optimisée

  • Sauvegarde instantanée sans rechargement de page
  • Feedback visuel : "⏳ Sauvegarde..." → "✓ Note enregistrée"
  • Disparition automatique du message après 2 secondes

Note optionnelle

La note est facultative. Si l'agent ne la renseigne pas, la valeur reste NULL.

Photos & Preuves — badge EXIF

Badge en bas de chaque photo (photo principale + miniatures), même style pilule rounded-full que « SIGNALEMENT INITIAL », affichant date/heure et coordonnées GPS si présentes en EXIF :

$photo['exif_datetime'] = $photo['exif_taken_at'] ? date('d/m/Y à H:i', strtotime($photo['exif_taken_at'])) : null;

Les données viennent directement des colonnes photos_incident.exif_taken_at/exif_latitude/exif_longitude (extraites à l'upload, voir Face Blurring — EXIF) — plus de re-lecture EXIF du fichier stocké à l'affichage. Les photos uploadées avant ce changement n'ont pas ces colonnes renseignées : le badge n'apparaît simplement pas (pas d'erreur).

Les <img> pointent vers /api/get_photo.php?id=<photo_id> (déchiffrement à la volée) au lieu du chemin filepath brut — voir api/get_photo.php.


statistiques.php — Statistiques temporelles (v3.1, 2026-08-25)

KPIs sur la période

Filtrée par plage de dates (défaut : 30 derniers jours) + commune si collectivité. Respecte le même filtrage par permissions que incidents.php (getMairieIdsForUser(), getAccessibleTypeIds()) :

KPI Calcul
Incidents (période) COUNT(*) sur created_at dans la plage
Résolus SUM(statut = 'resolu')
Taux de résolution résolus / total * 100
Temps moyen résolution AVG(TIMESTAMPDIFF(HOUR, created_at, date_resolution))
SLA dépassés (en cours) Même expression que le badge SLA d'incidents.php

Graphiques

  • Évolution hebdomadaire (créés vs résolus) : deux requêtes groupées par semaine ISO (YEARWEEK(…, 3)), l'une sur created_at, l'autre sur date_resolution, fusionnées côté PHP pour aligner les semaines (zéro si absente d'un des deux jeux).
  • Répartition par statut et par type : donuts.
  • Rendu via Chart.js (https://unpkg.com/chart.js@4) — domaine déjà whitelisté dans la CSP (script-src … https://unpkg.com), aucune modification nécessaire.

Tableaux de performance

Par service et par type : nb incidents, temps moyen de résolution, taux de résolution (type), SLA dépassés (service).

Export PDF — impression navigateur, pas de dépendance PHP

Aucune librairie PDF n'est présente côté PHP (dompdf/tcpdf/mpdf absents). Le bouton « Exporter en PDF » déclenche window.print(), avec une feuille de style dédiée qui masque sidebar/topbar/filtres :

@media print {
    header, aside, .no-print { display: none !important; }
    main { margin-left: 0 !important; padding: 0 !important; }
}

L'utilisateur choisit « Enregistrer en PDF » dans la boîte d'impression du navigateur.

Non couvert (hors périmètre initial)

Détail par commune pour les collectivités (juste un filtre global, pas de répartition ligne par ligne), filtres additionnels (type/service) sur cette page.


citoyens.php - Gestion des Usagers

Statistiques

Affiche 4 indicateurs :

Statistique Description
Total usagers Citoyens ayant signalé dans les mairies accessibles
Anonymes Citoyens avec anonyme = 1
Identifiés Citoyens avec anonyme = 0
Signalements Nombre total d'incidents associés

Liste des Usagers

Colonnes affichées :

Colonne Contenu
ID Device ID tronqué (12 premiers caractères)
Statut Badge Anonyme / Identifié
Identité Nom + Prénom déchiffrés (ou "Non renseigné")
Email Email déchiffré avec lien mailto
Téléphone Téléphone déchiffré
Signalements Bouton expansible avec compteur
Note qualité Moyenne des notes (0-5) avec code couleur
Inscription Date de création

Chiffrement des Données

Les données personnelles sont stockées chiffrées et déchiffrées à l'affichage :

$encryption = EncryptionManager::getInstance();

// Déchiffrement à l'affichage
$prenom = $encryption->decrypt($citoyen['prenom']);
$nom = $encryption->decrypt($citoyen['nom']);
$email = $encryption->decrypt($citoyen['email']);
$telephone = $encryption->decrypt($citoyen['telephone']);

Moyenne des Notes de Qualité

La colonne "Note qualité" affiche la moyenne des notes attribuées aux signalements de l'usager :

Moyenne Couleur Signification
≥ 4.0 🟢 Vert Bon contributeur
2.5 - 3.9 🟠 Orange Qualité moyenne
< 2.5 🔴 Rouge Qualité insuffisante
SELECT
    c.*,
    AVG(i.note_qualite_signalement) as moyenne_notes,
    COUNT(i.note_qualite_signalement) as nb_notes
FROM citoyens c
INNER JOIN incidents i ON i.citoyen_id = c.id
WHERE i.mairie_id IN (?)
GROUP BY c.id

Utilisation

Cette moyenne permet d'identifier les usagers faisant des signalements de qualité et ceux nécessitant une sensibilisation.

Liste Expansible des Incidents

Cliquer sur le nombre de signalements affiche un tableau détaillé :

function toggleIncidents(citoyenId) {
    const row = document.getElementById('incidents-' + citoyenId);
    row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
}

Le tableau affiche pour chaque incident : - ID - Type (avec couleur) - Adresse - Statut (badge coloré) - Date - Bouton "Voir" → ouvre modal

Un clic sur "Voir" ouvre l'incident dans une modal iframe :

function openIncidentModal(incidentId) {
    const iframe = document.getElementById('incidentIframe');
    iframe.src = '/incident_detail.php?id=' + incidentId + '&modal=1';
    modal.style.display = 'block';
}

Fermeture : Touche Escape ou clic extérieur.


groupes.php - Groupes de Signalements

Définition d'un Groupe Valide

Un groupe de signalements doit respecter tous ces critères :

Critère Condition SQL
Même type GROUP BY type_id
Même localisation GROUP BY group_id (coordonnées similaires)
Usagers différents HAVING COUNT(DISTINCT citoyen_id) >= 2

Critère Multi-Usagers

Un signalement fait plusieurs fois par le même citoyen ne constitue pas un groupe. Il faut au minimum 2 citoyens distincts pour former un groupe valide.

Requête SQL

SELECT
    i.group_id,
    COUNT(DISTINCT i.id) as nb_signalements,
    COUNT(DISTINCT i.citoyen_id) as nb_citoyens,
    MIN(i.created_at) as premier_signalement,
    MAX(i.created_at) as dernier_signalement,
    t.nom as type_nom,
    t.couleur as type_couleur,
    MAX(i.adresse) as adresse,
    MAX(i.latitude) as latitude,
    MAX(i.longitude) as longitude,
    SUM(CASE WHEN i.statut = 'nouveau' THEN 1 ELSE 0 END) as nb_nouveaux,
    SUM(CASE WHEN i.statut = 'en_cours' THEN 1 ELSE 0 END) as nb_en_cours,
    SUM(CASE WHEN i.statut = 'resolu' THEN 1 ELSE 0 END) as nb_resolus
FROM incidents i
JOIN types_incident t ON i.type_id = t.id
WHERE i.mairie_id = ?
AND i.group_id IS NOT NULL
GROUP BY i.group_id, t.nom, t.couleur
HAVING nb_citoyens >= 2
ORDER BY nb_signalements DESC

Statistiques Affichées

Carte Calcul
Total groupes count($groupes)
Signalements multiples Groupes avec nb_signalements > 1
Total signalements Somme des nb_signalements
Taux groupement (nb_groupes / total_signalements) * 100

Filtres Disponibles

  • Tous les groupes
  • Signalements multiples uniquement
  • Avec statut "Nouveau"
  • Avec statut "En cours"

Permissions et Filtrage

Toutes les pages respectent le système de permissions basé sur les services :

$auth = new Auth();
$auth->requireAuth();

// Récupération des mairies accessibles
$mairieIds = $auth->getMairieIdsForUser();

// Filtrage des types accessibles
$accessibleTypeIds = $auth->getAccessibleTypeIds();

Voir Système de permissions pour plus de détails.