Backoffice - Informations Officielles¶
Télécharger cette page en Markdown
Accès¶
URL : https://urbafix.fr/infos.php
Menu : Tableau de bord → 📢 Infos officielles
Rôles autorisés : admin, gestionnaire, superadmin
Interface¶
Vue d'ensemble¶
┌─────────────────────────────────────────────────────────────┐
│ 📢 Informations Officielles [+ Nouveau] │
├─────────────────────────────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 3 │ │ 2 │ │ 5 │ │ 1 │ ← Statistiques │
│ │Brouil│ │Progr.│ │Publié│ │Expiré│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────────────────────┤
│ Filtres: [Statut ▼] [Type ▼] [Commune ▼] [Rechercher] │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CARTE LEAFLET │ │
│ │ ○ Info 1 (rayon 500m) ○ Info 2 (rayon 1km) │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 📋 Liste des informations │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🚧 Travaux rue Victor Hugo [PUBLIE] │ │
│ │ 📍 Menton • 500m • Expire: 30/01 │ │
│ │ [✏️ Modifier] [🔴 Désactiver] │ │
│ └─────────────────────────────────────────────────────┘ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ░ 🎉 Marché de Noël (texte atténué) [EXPIRE] ░░░░░░░ │ ← hachuré
│ ░ [✏️ Modifier] ░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
└─────────────────────────────────────────────────────────────┘
Hachurage des publications inactives¶
Les cards dont la publication est terminée ou désactivée sont visuellement marquées par un hachurage diagonal gris :
| Condition | Rendu |
|---|---|
statut = 'EXPIRE' |
Card hachurée, texte atténué |
statut = 'DESACTIVE' |
Card hachurée, texte atténué |
date_expiration dépassée (quel que soit le statut) |
Card hachurée, texte atténué |
| Sinon | Card normale |
Le badge de statut et les boutons d'action restent à pleine opacité pour rester utilisables.
$isInactive = in_array($info['statut'], ['EXPIRE', 'DESACTIVE'])
|| ($info['date_expiration'] && strtotime($info['date_expiration']) < time());
.info-inactive {
background-image: repeating-linear-gradient(
-45deg,
transparent, transparent 6px,
rgba(107, 114, 128, 0.07) 6px,
rgba(107, 114, 128, 0.07) 12px
);
}
.info-inactive .info-text { color: rgba(0, 0, 0, 0.4); }
Statistiques¶
Cards en haut de page affichant les compteurs par statut :
| Card | Couleur | Filtre SQL |
|---|---|---|
| Brouillons | Gris | statut = 'BROUILLON' |
| Programmés | Bleu | statut = 'PROGRAMME' |
| Publiés | Vert | statut = 'PUBLIE' |
| Expirés | Orange | statut = 'EXPIRE' |
Filtres¶
Par statut¶
$statut = $_GET['statut'] ?? '';
if ($statut) {
$where[] = "statut = :statut";
$params[':statut'] = $statut;
}
Options : Tous, BROUILLON, PROGRAMME, PUBLIE, EXPIRE, DESACTIVE
Par type¶
$type = $_GET['type'] ?? '';
if ($type) {
$where[] = "type_info = :type";
$params[':type'] = $type;
}
Options : Tous, TRAVAUX, EVENEMENT, ALERTE, ANNONCE
Par commune¶
Dropdown des mairies accessibles à l'utilisateur (selon rôle et permissions).
Recherche texte¶
$search = $_GET['search'] ?? '';
if ($search) {
$where[] = "(titre LIKE :search OR description LIKE :search OR adresse LIKE :search)";
$params[':search'] = '%' . $search . '%';
}
Carte Leaflet¶
Initialisation¶
const map = L.map('map').setView([43.7, 7.25], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
Affichage des infos¶
infos.forEach(info => {
// Marqueur
const marker = L.marker([info.latitude, info.longitude])
.bindPopup(`<b>${info.titre}</b><br>${info.adresse}`);
// Cercle de rayon
const circle = L.circle([info.latitude, info.longitude], {
radius: info.rayon_metres,
color: getColorByType(info.type_info),
fillOpacity: 0.2
});
marker.addTo(map);
circle.addTo(map);
});
function getColorByType(type) {
const colors = {
'TRAVAUX': '#F59E0B',
'EVENEMENT': '#8B5CF6',
'ALERTE': '#EF4444',
'ANNONCE': '#3B82F6'
};
return colors[type] || '#6B7280';
}
Modal Création/Édition¶
Structure¶
<div class="modal" id="infoModal">
<div class="modal-content">
<h2>Nouvelle information</h2>
<form id="infoForm">
<!-- Titre -->
<input type="text" name="titre" required maxlength="255">
<!-- Type -->
<select name="type_info" required>
<option value="TRAVAUX">🚧 Travaux</option>
<option value="EVENEMENT">🎉 Événement</option>
<option value="ALERTE">⚠️ Alerte</option>
<option value="ANNONCE">📢 Annonce</option>
</select>
<!-- Description -->
<textarea name="description" required rows="4"></textarea>
<!-- Carte interactive -->
<div id="modalMap" style="height: 300px;"></div>
<input type="hidden" name="latitude">
<input type="hidden" name="longitude">
<!-- Adresse -->
<input type="text" name="adresse">
<!-- Rayon -->
<input type="number" name="rayon_metres" value="1000" min="100" max="50000">
<!-- Publication différée -->
<input type="datetime-local" name="date_debut_publication">
<!-- Expiration -->
<input type="datetime-local" name="date_expiration">
<button type="submit">Enregistrer</button>
</form>
</div>
</div>
Carte interactive¶
const modalMap = L.map('modalMap').setView([43.7, 7.25], 12);
let marker = null;
let circle = null;
modalMap.on('click', function(e) {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
// Mettre à jour les champs
document.querySelector('[name="latitude"]').value = lat;
document.querySelector('[name="longitude"]').value = lng;
// Marqueur
if (marker) marker.remove();
marker = L.marker([lat, lng]).addTo(modalMap);
// Cercle de rayon
updateRadiusCircle(lat, lng);
});
function updateRadiusCircle(lat, lng) {
const rayon = parseInt(document.querySelector('[name="rayon_metres"]').value);
if (circle) circle.remove();
circle = L.circle([lat, lng], {
radius: rayon,
color: '#3B82F6',
fillOpacity: 0.2
}).addTo(modalMap);
}
Actions¶
Créer¶
- Cliquer sur [+ Nouveau]
- Remplir le formulaire
- Cliquer sur la carte pour positionner
- Enregistrer → Statut
BROUILLON
Modifier¶
- Cliquer sur [✏️ Modifier]
- Modal pré-remplie
- Modifier les champs
- Enregistrer
Restrictions
Les infos PUBLIE ne peuvent être modifiées que pour la date d'expiration.
Publier¶
- Cliquer sur [▶️ Publier]
- Confirmation
- Selon
date_debut_publication: - NULL →
PUBLIEimmédiatement - Future →
PROGRAMME
Désactiver¶
- Cliquer sur [🔴 Désactiver]
- Confirmation
- Statut →
DESACTIVE
Supprimer¶
- Cliquer sur [🗑️ Supprimer]
- Double confirmation (action irréversible)
- Suppression en base + photos
Code PHP¶
Liste des infos¶
<?php
require_once 'includes/auth.php';
require_once 'backend/src/Database.php';
$db = Database::getInstance();
$user = Auth::getUser();
// Récupérer la mairie de l'utilisateur
$mairieId = $user['mairie_id'];
// Requête avec filtres
$sql = "
SELECT i.*, m.nom as mairie_nom
FROM informations_officielles i
JOIN mairies m ON i.mairie_id = m.id
WHERE i.mairie_id = :mairie_id
";
$params = [':mairie_id' => $mairieId];
// Appliquer les filtres...
$sql .= " ORDER BY i.created_at DESC";
$infos = $db->fetchAll($sql, $params);
?>
Création¶
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'mairie_id' => $user['mairie_id'],
'titre' => trim($_POST['titre']),
'description' => trim($_POST['description']),
'type_info' => $_POST['type_info'],
'latitude' => (float)$_POST['latitude'],
'longitude' => (float)$_POST['longitude'],
'adresse' => trim($_POST['adresse']),
'rayon_metres' => (int)($_POST['rayon_metres'] ?? 1000),
'date_debut_publication' => $_POST['date_debut_publication'] ?: null,
'date_expiration' => $_POST['date_expiration'] ?: null,
'created_by' => $user['email']
];
$sql = "
INSERT INTO informations_officielles
(mairie_id, titre, description, type_info, latitude, longitude,
adresse, rayon_metres, statut, date_debut_publication,
date_expiration, created_by)
VALUES
(:mairie_id, :titre, :description, :type_info, :latitude, :longitude,
:adresse, :rayon_metres, 'BROUILLON', :date_debut_publication,
:date_expiration, :created_by)
";
$infoId = $db->insert($sql, $data);
header('Location: infos.php?success=created');
exit;
}
?>
Responsive¶
Mobile (< 768px)¶
- Carte masquée par défaut (toggle)
- Liste en cards pleine largeur
- Filtres en accordéon
- Modal plein écran
Desktop (>= 768px)¶
- Carte visible (50% largeur)
- Liste à côté (50% largeur)
- Filtres en ligne
- Modal centrée (600px max)
Sécurité¶
Vérifications¶
// Authentification
Auth::requireLogin();
// Rôle minimum
if (!in_array($user['role'], ['admin', 'gestionnaire', 'superadmin'])) {
header('HTTP/1.1 403 Forbidden');
exit('Accès refusé');
}
// Accès mairie
if ($user['role'] !== 'superadmin' && $info['mairie_id'] !== $user['mairie_id']) {
header('HTTP/1.1 403 Forbidden');
exit('Accès refusé à cette information');
}
Protection CSRF¶
// Générer token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// Vérifier
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Token CSRF invalide');
}