Aller au contenu

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';
}

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

  1. Cliquer sur [+ Nouveau]
  2. Remplir le formulaire
  3. Cliquer sur la carte pour positionner
  4. Enregistrer → Statut BROUILLON

Modifier

  1. Cliquer sur [✏️ Modifier]
  2. Modal pré-remplie
  3. Modifier les champs
  4. Enregistrer

Restrictions

Les infos PUBLIE ne peuvent être modifiées que pour la date d'expiration.

Publier

  1. Cliquer sur [▶️ Publier]
  2. Confirmation
  3. Selon date_debut_publication :
  4. NULL → PUBLIE immédiatement
  5. Future → PROGRAMME

Désactiver

  1. Cliquer sur [🔴 Désactiver]
  2. Confirmation
  3. Statut → DESACTIVE

Supprimer

  1. Cliquer sur [🗑️ Supprimer]
  2. Double confirmation (action irréversible)
  3. 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');
}

Validation entrées

$titre = htmlspecialchars(trim($_POST['titre']), ENT_QUOTES, 'UTF-8');
$latitude = filter_var($_POST['latitude'], FILTER_VALIDATE_FLOAT);
$longitude = filter_var($_POST['longitude'], FILTER_VALIDATE_FLOAT);

if ($latitude < -90 || $latitude > 90) {
    throw new Exception('Latitude invalide');
}