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 surcreated_at, l'autre surdate_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 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
Modal Incident¶
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.