Files
geo/api/src/Controllers/OperationController.php
pierre 599b9fcda0 feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 11:01:45 +02:00

1517 lines
57 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Controllers;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ExportService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/../Services/OperationDataService.php';
use PDO;
use PDOException;
use Database;
use AppConfig;
use Request;
use Response;
use Session;
use LogService;
use ExportService;
use ApiService;
use OperationDataService;
use Exception;
use DateTime;
class OperationController {
private PDO $db;
private AppConfig $appConfig;
public function __construct() {
$this->db = Database::getInstance();
$this->appConfig = AppConfig::getInstance();
}
/**
* Récupère l'entité de l'utilisateur connecté
*
* @param int $userId ID de l'utilisateur
* @return int|null ID de l'entité ou null si non trouvé
*/
private function getUserEntiteId(int $userId): ?int {
try {
$stmt = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ? (int)$user['fk_entite'] : null;
} catch (Exception $e) {
LogService::log('Erreur lors de la récupération de l\'entité utilisateur', [
'level' => 'error',
'error' => $e->getMessage(),
'userId' => $userId
]);
return null;
}
}
/**
* Valide les données d'une opération
*
* @param array $data Données à valider
* @param int $entiteId ID de l'entité
* @param int|null $operationId ID de l'opération (pour update)
* @return array|null Erreurs de validation ou null
*/
private function validateOperationData(array $data, int $entiteId, ?int $operationId = null): ?array {
$errors = [];
// Validation du libellé (accepter 'name' ou 'libelle')
$libelle = $data['libelle'] ?? $data['name'] ?? null;
if (!$libelle || empty(trim($libelle))) {
$errors[] = 'Le nom de l\'opération est obligatoire';
} else {
$libelle = trim($libelle);
if (strlen($libelle) < 5) {
$errors[] = 'Le nom de l\'opération doit contenir au moins 5 caractères';
}
if (strlen($libelle) > 75) {
$errors[] = 'Le nom de l\'opération ne peut pas dépasser 75 caractères';
}
// Vérifier l'unicité du nom dans l'entité
$sql = 'SELECT COUNT(*) as count FROM operations WHERE fk_entite = ? AND libelle = ? AND chk_active = 1';
$params = [$entiteId, $libelle];
if ($operationId) {
$sql .= ' AND id != ?';
$params[] = $operationId;
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result && $result['count'] > 0) {
$errors[] = 'Une opération avec ce nom existe déjà dans votre entité';
}
}
// Validation des dates
if (!isset($data['date_deb']) || empty($data['date_deb'])) {
$errors[] = 'La date de début est obligatoire';
}
if (!isset($data['date_fin']) || empty($data['date_fin'])) {
$errors[] = 'La date de fin est obligatoire';
}
if (
isset($data['date_deb']) && isset($data['date_fin']) &&
!empty($data['date_deb']) && !empty($data['date_fin'])
) {
$dateDeb = DateTime::createFromFormat('Y-m-d', $data['date_deb']);
$dateFin = DateTime::createFromFormat('Y-m-d', $data['date_fin']);
if (!$dateDeb) {
$errors[] = 'Format de date de début invalide (YYYY-MM-DD attendu)';
}
if (!$dateFin) {
$errors[] = 'Format de date de fin invalide (YYYY-MM-DD attendu)';
}
if ($dateDeb && $dateFin && $dateFin <= $dateDeb) {
$errors[] = 'La date de fin doit être postérieure à la date de début';
}
}
return empty($errors) ? null : $errors;
}
/**
* Récupère toutes les opérations de l'entité de l'utilisateur
*/
public function getOperations(): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$stmt = $this->db->prepare('
SELECT id, libelle, date_deb, date_fin, chk_distinct_sectors,
created_at, updated_at, chk_active
FROM operations
WHERE fk_entite = ?
ORDER BY chk_active DESC, created_at DESC
');
$stmt->execute([$entiteId]);
$operations = $stmt->fetchAll(PDO::FETCH_ASSOC);
Response::json([
'status' => 'success',
'operations' => $operations
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la récupération des opérations', [
'level' => 'error',
'error' => $e->getMessage(),
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la récupération des opérations'
], 500);
}
}
/**
* Récupère une opération spécifique par son ID
*/
public function getOperationById(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
$stmt = $this->db->prepare('
SELECT id, libelle, date_deb, date_fin, chk_distinct_sectors,
created_at, updated_at, chk_active
FROM operations
WHERE id = ? AND fk_entite = ?
');
$stmt->execute([$operationId, $entiteId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
Response::json([
'status' => 'success',
'operation' => $operation
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la récupération de l\'opération', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la récupération de l\'opération'
], 500);
}
}
/**
* Crée une nouvelle opération avec duplication des données de l'opération active précédente
*/
public function createOperation(): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$data = Request::getJson();
// Validation des données
$errors = $this->validateOperationData($data, $entiteId);
if ($errors) {
Response::json([
'status' => 'error',
'message' => 'Erreurs de validation',
'errors' => $errors
], 400);
return;
}
$this->db->beginTransaction();
// Étape 1 : Récupérer l'id de l'opération active actuelle (oldOpeId)
$stmt = $this->db->prepare('
SELECT id FROM operations
WHERE fk_entite = ? AND chk_active = 1
LIMIT 1
');
$stmt->execute([$entiteId]);
$oldOperation = $stmt->fetch(PDO::FETCH_ASSOC);
$oldOpeId = $oldOperation ? (int)$oldOperation['id'] : null;
LogService::log('Étape 1 : Récupération opération active', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'oldOpeId' => $oldOpeId
]);
// Étape 2 : Créer la nouvelle opération (newOpeId) - pas encore active
$stmt = $this->db->prepare('
INSERT INTO operations (
fk_entite, libelle, date_deb, date_fin,
chk_distinct_sectors, fk_user_creat, chk_active
) VALUES (?, ?, ?, ?, ?, ?, 0)
');
$stmt->execute([
$entiteId,
trim($data['name']),
$data['date_deb'],
$data['date_fin'],
isset($data['chk_distinct_sectors']) ? (int)$data['chk_distinct_sectors'] : 0,
$userId
]);
$newOpeId = (int)$this->db->lastInsertId();
LogService::log('Étape 2 : Création nouvelle opération', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'newOpeId' => $newOpeId,
'libelle' => trim($data['name'])
]);
// Étape 3 : Insérer tous les users actifs de l'entité dans ope_users avec newOpeId
$stmt = $this->db->prepare('
INSERT INTO ope_users (fk_operation, fk_user, fk_role, first_name, encrypted_name, sect_name, fk_user_creat)
SELECT ?, id, fk_role, first_name, encrypted_name, sect_name, ?
FROM users
WHERE fk_entite = ? AND chk_active = 1
');
$stmt->execute([$newOpeId, $userId, $entiteId]);
$insertedUsers = $stmt->rowCount();
LogService::log('Étape 3 : Insertion users actifs', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'newOpeId' => $newOpeId,
'insertedUsers' => $insertedUsers
]);
// Étape 4 : Si oldOpeId existe, dupliquer les secteurs et données associées
$duplicatedSectors = 0;
$duplicatedUsersSectors = 0;
$duplicatedPassages = 0;
if ($oldOpeId) {
// Étape 4.1 : Récupérer tous les secteurs de l'ancienne opération
$stmt = $this->db->prepare('
SELECT id, libelle, sector, color
FROM ope_sectors
WHERE fk_operation = ? AND chk_active = 1
');
$stmt->execute([$oldOpeId]);
$oldSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($oldSectors as $oldSector) {
$oldSectId = (int)$oldSector['id'];
// Étape 4.2 : Dupliquer le secteur avec newOpeId
$stmt = $this->db->prepare('
INSERT INTO ope_sectors (fk_operation, fk_old_sector, libelle, sector, color, fk_user_creat)
VALUES (?, ?, ?, ?, ?, ?)
');
$stmt->execute([
$newOpeId,
$oldSectId,
$oldSector['libelle'],
$oldSector['sector'],
$oldSector['color'],
$userId
]);
$newSectId = (int)$this->db->lastInsertId();
$duplicatedSectors++;
// Étape 4.3 : Dupliquer les users_sectors en vérifiant que fk_user existe dans ope_users
$stmt = $this->db->prepare('
INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, fk_user_creat)
SELECT ?, ous.fk_user, ?, ?
FROM ope_users_sectors ous
INNER JOIN ope_users ou ON ou.fk_user = ous.fk_user AND ou.fk_operation = ?
WHERE ous.fk_operation = ? AND ous.fk_sector = ? AND ous.chk_active = 1
');
$stmt->execute([$newOpeId, $newSectId, $userId, $newOpeId, $oldOpeId, $oldSectId]);
$duplicatedUsersSectors += $stmt->rowCount();
// Étape 4.4 : Dupliquer les passages avec les valeurs par défaut spécifiées
$stmt = $this->db->prepare('
INSERT INTO ope_pass (
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
fk_user_creat, chk_active
)
SELECT
?, ?, fk_user, fk_adresse, numero, rue, rue_bis, ville,
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
2, NULL, 0, 4, 0, 0, 0, 1, 0, 0, 1, 0, ?, 1
FROM ope_pass
WHERE fk_operation = ? AND fk_sector = ? AND chk_active = 1
');
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $oldSectId]);
$duplicatedPassages += $stmt->rowCount();
}
LogService::log('Étape 4 : Duplication données anciennes opération', [
'level' => 'info',
'userId' => $userId,
'oldOpeId' => $oldOpeId,
'newOpeId' => $newOpeId,
'duplicatedSectors' => $duplicatedSectors,
'duplicatedUsersSectors' => $duplicatedUsersSectors,
'duplicatedPassages' => $duplicatedPassages
]);
}
// Étape 5 : Désactiver l'ancienne opération
if ($oldOpeId) {
$stmt = $this->db->prepare('
UPDATE operations
SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
WHERE id = ?
');
$stmt->execute([$userId, $oldOpeId]);
LogService::log('Étape 5 : Désactivation ancienne opération', [
'level' => 'info',
'userId' => $userId,
'oldOpeId' => $oldOpeId
]);
}
// Étape 6 : Activer la nouvelle opération
$stmt = $this->db->prepare('
UPDATE operations
SET chk_active = 1, updated_at = NOW(), fk_user_modif = ?
WHERE id = ?
');
$stmt->execute([$userId, $newOpeId]);
LogService::log('Étape 6 : Activation nouvelle opération', [
'level' => 'info',
'userId' => $userId,
'newOpeId' => $newOpeId
]);
$this->db->commit();
// Étape 7 : Préparer la réponse avec les groupes JSON
$response = OperationDataService::prepareOperationResponse($this->db, $newOpeId, $entiteId);
LogService::log('Création opération terminée avec succès', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'newOpeId' => $newOpeId,
'oldOpeId' => $oldOpeId,
'stats' => [
'insertedUsers' => $insertedUsers,
'duplicatedSectors' => $duplicatedSectors,
'duplicatedUsersSectors' => $duplicatedUsersSectors,
'duplicatedPassages' => $duplicatedPassages
]
]);
Response::json($response, 201);
} catch (Exception $e) {
$this->db->rollBack();
LogService::log('Erreur lors de la création de l\'opération', [
'level' => 'error',
'error' => $e->getMessage(),
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la création de l\'opération'
], 500);
}
}
/**
* Met à jour une opération (uniquement l'opération active)
*/
public function updateOperation(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
$data = Request::getJson();
// Étape 1: Vérifier que l'opération existe (sans filtrer par entité d'abord)
$stmt = $this->db->prepare('
SELECT id, fk_entite, chk_active
FROM operations
WHERE id = ?
');
$stmt->execute([$operationId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
LogService::log('Tentative de mise à jour d\'une opération inexistante', [
'level' => 'warning',
'userId' => $userId,
'operationId' => $operationId
]);
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
// Étape 2: Vérifier la cohérence du fk_entite si fourni dans le JSON
if (isset($data['fk_entite'])) {
$receivedEntiteId = (int)$data['fk_entite'];
$operationEntiteId = (int)$operation['fk_entite'];
if ($receivedEntiteId !== $operationEntiteId) {
LogService::log('Incohérence détectée entre fk_entite reçu et celui de l\'opération', [
'level' => 'warning',
'userId' => $userId,
'operationId' => $operationId,
'receivedEntiteId' => $receivedEntiteId,
'operationEntiteId' => $operationEntiteId
]);
Response::json([
'status' => 'error',
'message' => 'Incohérence détectée : l\'opération n\'appartient pas à l\'entité spécifiée'
], 400);
return;
}
}
// Étape 3: Vérifier que l'utilisateur a accès à l'entité de l'opération
$operationEntiteId = (int)$operation['fk_entite'];
if ($operationEntiteId !== $entiteId) {
LogService::log('Tentative d\'accès à une opération d\'une autre entité', [
'level' => 'warning',
'userId' => $userId,
'userEntiteId' => $entiteId,
'operationEntiteId' => $operationEntiteId,
'operationId' => $operationId
]);
Response::json([
'status' => 'error',
'message' => 'Vous n\'avez pas accès à cette entité'
], 403);
return;
}
// Étape 4: Vérifier que l'opération est active
if (!$operation['chk_active']) {
LogService::log('Tentative de modification d\'une opération inactive', [
'level' => 'warning',
'userId' => $userId,
'operationId' => $operationId
]);
Response::json([
'status' => 'error',
'message' => 'Seule l\'opération active peut être modifiée'
], 403);
return;
}
// Validation des données
$errors = $this->validateOperationData($data, $entiteId, $operationId);
if ($errors) {
Response::json([
'status' => 'error',
'message' => 'Erreurs de validation',
'errors' => $errors
], 400);
return;
}
// Mettre à jour l'opération
$stmt = $this->db->prepare('
UPDATE operations
SET libelle = ?, date_deb = ?, date_fin = ?,
chk_distinct_sectors = ?, updated_at = NOW(), fk_user_modif = ?
WHERE id = ?
');
$libelle = trim($data['libelle'] ?? $data['name']);
$stmt->execute([
$libelle,
$data['date_deb'],
$data['date_fin'],
isset($data['chk_distinct_sectors']) ? (int)$data['chk_distinct_sectors'] : 0,
$userId,
$operationId
]);
LogService::log('Mise à jour d\'une opération', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'operationId' => $operationId
]);
Response::json([
'status' => 'success',
'message' => 'Opération mise à jour avec succès'
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la mise à jour de l\'opération', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la mise à jour de l\'opération'
], 500);
}
}
/**
* Désactive une opération
*/
public function deleteOperation(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$operationId = (int)$id;
// Récupérer les informations de l'utilisateur (rôle et entité)
$stmt = $this->db->prepare('SELECT fk_entite, fk_role FROM users WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur non trouvé'
], 404);
return;
}
$userEntiteId = (int)$user['fk_entite'];
$userRole = (int)$user['fk_role'];
// Vérifier que l'utilisateur a un rôle > 1 (pas un simple utilisateur)
if ($userRole <= 1) {
LogService::log('Tentative de suppression d\'opération avec rôle insuffisant', [
'level' => 'warning',
'userId' => $userId,
'userRole' => $userRole,
'operationId' => $operationId
]);
Response::json([
'status' => 'error',
'message' => 'Vous n\'avez pas les droits suffisants pour supprimer une opération'
], 403);
return;
}
// Récupérer les informations de l'opération
$stmt = $this->db->prepare('
SELECT id, fk_entite, chk_active
FROM operations
WHERE id = ?
');
$stmt->execute([$operationId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
$operationEntiteId = (int)$operation['fk_entite'];
// Si l'utilisateur a le rôle 2, vérifier qu'il appartient à la même entité que l'opération
if ($userRole == 2 && $userEntiteId !== $operationEntiteId) {
LogService::log('Tentative de suppression d\'opération d\'une autre entité', [
'level' => 'warning',
'userId' => $userId,
'userRole' => $userRole,
'userEntiteId' => $userEntiteId,
'operationEntiteId' => $operationEntiteId,
'operationId' => $operationId
]);
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez supprimer que les opérations de votre entité'
], 403);
return;
}
// Les utilisateurs avec rôle > 2 (super admin, etc.) peuvent supprimer toutes les opérations
// Les utilisateurs avec rôle 2 ne peuvent supprimer que les opérations de leur entité
$operationActive = (bool)$operation['chk_active'];
// Créer un export complet automatique avant suppression (Excel + JSON)
try {
$exportService = new ExportService();
// Générer l'export Excel
$excelFile = $exportService->generateExcelExport($operationId, $operationEntiteId);
// Générer l'export JSON
$jsonFile = $exportService->generateJsonExport($operationId, $operationEntiteId, 'auto');
LogService::log('Export complet automatique créé avant suppression', [
'level' => 'info',
'userId' => $userId,
'operationId' => $operationId,
'operationActive' => $operationActive,
'excelFile' => $excelFile['filename'],
'jsonFile' => $jsonFile['filename']
]);
} catch (Exception $e) {
LogService::log('Erreur lors de l\'export complet automatique avant suppression', [
'level' => 'warning',
'error' => $e->getMessage(),
'operationId' => $operationId,
'operationActive' => $operationActive
]);
// On continue même si l'export échoue
}
// Commencer une transaction pour supprimer toutes les données liées
$this->db->beginTransaction();
try {
// 1. Supprimer les médias liés à l'opération
$stmt = $this->db->prepare('DELETE FROM medias WHERE support = "operation" AND support_id = ?');
$stmt->execute([$operationId]);
$deletedMedias = $stmt->rowCount();
// 2. Supprimer l'historique des passages (via les passages de l'opération)
$stmt = $this->db->prepare('
DELETE oph FROM ope_pass_histo oph
INNER JOIN ope_pass op ON oph.fk_pass = op.id
WHERE op.fk_operation = ?
');
$stmt->execute([$operationId]);
$deletedPassHisto = $stmt->rowCount();
// 3. Supprimer les passages
$stmt = $this->db->prepare('DELETE FROM ope_pass WHERE fk_operation = ?');
$stmt->execute([$operationId]);
$deletedPass = $stmt->rowCount();
// 4. Supprimer les relations utilisateurs-secteurs
$stmt = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_operation = ?');
$stmt->execute([$operationId]);
$deletedUsersSectors = $stmt->rowCount();
// 5. Supprimer les adresses des secteurs (via les secteurs de l'opération)
$stmt = $this->db->prepare('
DELETE sa FROM sectors_adresses sa
INNER JOIN ope_sectors os ON sa.fk_sector = os.id
WHERE os.fk_operation = ?
');
$stmt->execute([$operationId]);
$deletedSectorsAdresses = $stmt->rowCount();
// 6. Supprimer les secteurs
$stmt = $this->db->prepare('DELETE FROM ope_sectors WHERE fk_operation = ?');
$stmt->execute([$operationId]);
$deletedSectors = $stmt->rowCount();
// 7. Supprimer les utilisateurs de l'opération
$stmt = $this->db->prepare('DELETE FROM ope_users WHERE fk_operation = ?');
$stmt->execute([$operationId]);
$deletedUsers = $stmt->rowCount();
// 8. Supprimer l'opération elle-même
$stmt = $this->db->prepare('DELETE FROM operations WHERE id = ?');
$stmt->execute([$operationId]);
// Valider la transaction
$this->db->commit();
LogService::log('Suppression complète d\'une opération et de toutes ses données', [
'level' => 'info',
'userId' => $userId,
'userRole' => $userRole,
'userEntiteId' => $userEntiteId,
'operationEntiteId' => $operationEntiteId,
'operationId' => $operationId,
'operationActive' => $operationActive,
'deletedCounts' => [
'medias' => $deletedMedias,
'ope_pass_histo' => $deletedPassHisto,
'ope_pass' => $deletedPass,
'ope_users_sectors' => $deletedUsersSectors,
'sectors_adresses' => $deletedSectorsAdresses,
'ope_sectors' => $deletedSectors,
'ope_users' => $deletedUsers,
'operations' => 1
]
]);
// Préparer la réponse selon le statut de l'opération supprimée
$response = [
'status' => 'success',
'message' => 'Opération et toutes ses données supprimées avec succès',
'operation_was_active' => $operationActive,
'deleted_counts' => [
'medias' => $deletedMedias,
'passages_history' => $deletedPassHisto,
'passages' => $deletedPass,
'user_sectors' => $deletedUsersSectors,
'sectors_addresses' => $deletedSectorsAdresses,
'sectors' => $deletedSectors,
'users' => $deletedUsers
]
];
// Si l'opération supprimée était active, activer la dernière opération créée
$newActiveOperationId = null;
if ($operationActive) {
// Trouver la dernière opération créée de cette entité
$stmt = $this->db->prepare('
SELECT id FROM operations
WHERE fk_entite = ?
ORDER BY id DESC
LIMIT 1
');
$stmt->execute([$operationEntiteId]);
$lastOperation = $stmt->fetch(PDO::FETCH_ASSOC);
if ($lastOperation) {
$newActiveOperationId = (int)$lastOperation['id'];
// Activer cette opération
$stmt = $this->db->prepare('
UPDATE operations
SET chk_active = 1, updated_at = NOW(), fk_user_modif = ?
WHERE id = ?
');
$stmt->execute([$userId, $newActiveOperationId]);
LogService::log('Activation automatique de la dernière opération après suppression', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $operationEntiteId,
'newActiveOperationId' => $newActiveOperationId,
'deletedOperationId' => $operationId
]);
}
}
// Récupérer les 3 dernières opérations (dont l'active)
$stmt = $this->db->prepare('
SELECT id, libelle, date_deb, date_fin, chk_distinct_sectors,
created_at, updated_at, chk_active
FROM operations
WHERE fk_entite = ?
ORDER BY chk_active DESC, created_at DESC
LIMIT 3
');
$stmt->execute([$operationEntiteId]);
$operations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['operations'] = $operations;
// Si une opération a été activée, récupérer ses données complètes
if ($newActiveOperationId) {
// Récupérer les secteurs de la nouvelle opération active
$stmt = $this->db->prepare('
SELECT id, libelle, color, sector, created_at, updated_at, chk_active
FROM ope_sectors
WHERE fk_operation = ? AND chk_active = 1
ORDER BY libelle
');
$stmt->execute([$newActiveOperationId]);
$sectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Récupérer les passages de la nouvelle opération active
$stmt = $this->db->prepare('
SELECT
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.fk_adresse,
p.passed_at, p.fk_type, p.numero, p.rue, p.rue_bis, p.ville,
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
p.chk_email_sent, p.docremis, p.date_repasser, p.nb_passages,
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active
FROM ope_pass p
WHERE p.fk_operation = ? AND p.chk_active = 1
ORDER BY p.created_at DESC
LIMIT 50
');
$stmt->execute([$newActiveOperationId]);
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Déchiffrer les données sensibles des passages
foreach ($passages as &$passage) {
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
$passage['email'] = !empty($passage['encrypted_email']) ?
ApiService::decryptSearchableData($passage['encrypted_email']) : '';
$passage['phone'] = !empty($passage['encrypted_phone']) ?
ApiService::decryptData($passage['encrypted_phone']) : '';
// Suppression des champs chiffrés
unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
}
// Récupérer les relations utilisateurs-secteurs
$stmt = $this->db->prepare('
SELECT
ous.id, ous.fk_operation, ous.fk_user, ous.fk_sector,
ous.created_at, ous.updated_at, ous.chk_active,
u.encrypted_name as user_name, u.first_name as user_first_name,
s.libelle as sector_name
FROM ope_users_sectors ous
INNER JOIN users u ON u.id = ous.fk_user
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
WHERE ous.fk_operation = ? AND ous.chk_active = 1
ORDER BY s.libelle, u.encrypted_name
');
$stmt->execute([$newActiveOperationId]);
$usersSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Déchiffrer les noms d'utilisateurs
foreach ($usersSectors as &$userSector) {
$userSector['user_name'] = ApiService::decryptData($userSector['user_name']);
unset($userSector['encrypted_name']);
}
$response['activated_operation'] = [
'id' => $newActiveOperationId,
'sectors' => $sectors,
'passages' => $passages,
'users_sectors' => $usersSectors
];
}
Response::json($response, 200);
} catch (Exception $e) {
// Annuler la transaction en cas d'erreur
$this->db->rollBack();
LogService::log('Erreur lors de la suppression complète de l\'opération', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $operationId,
'operationActive' => $operationActive,
'userId' => $userId
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la suppression complète de l\'opération'
], 500);
return;
}
} catch (Exception $e) {
LogService::log('Erreur lors de la suppression de l\'opération', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la suppression de l\'opération'
], 500);
}
}
/**
* Export Excel d'une opération (retourne directement le fichier)
*/
public function exportExcel(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
// Vérifier d'abord si l'opération existe (sans filtrer par entité)
$stmt = $this->db->prepare('
SELECT id, libelle, chk_active, fk_entite
FROM operations
WHERE id = ?
');
$stmt->execute([$operationId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
LogService::log('Opération inexistante pour export Excel', [
'level' => 'warning',
'operationId' => $operationId,
'userId' => $userId,
'entiteId' => $entiteId
]);
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
// Vérifier que l'opération appartient à l'entité de l'utilisateur
if ((int)$operation['fk_entite'] !== $entiteId) {
LogService::log('Tentative d\'accès à une opération d\'une autre entité pour export Excel', [
'level' => 'warning',
'operationId' => $operationId,
'operationEntiteId' => (int)$operation['fk_entite'],
'userEntiteId' => $entiteId,
'userId' => $userId
]);
Response::json([
'status' => 'error',
'message' => 'Vous n\'avez pas accès à cette opération'
], 403);
return;
}
// Paramètre optionnel pour filtrer par utilisateur
$filterUserId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null;
// Générer l'export Excel
$exportService = new ExportService();
$fileInfo = $exportService->generateExcelExport($operationId, $entiteId, $filterUserId);
// Construire le chemin complet du fichier
$filepath = getcwd() . '/' . $fileInfo['path'];
if (!file_exists($filepath)) {
Response::json([
'status' => 'error',
'message' => 'Fichier Excel non trouvé'
], 404);
return;
}
// Nettoyer le nom de l'opération pour le nom de fichier
$operationName = preg_replace('/[^a-zA-Z0-9\-_]/', '_', $operation['libelle']);
$userSuffix = $filterUserId ? "-user{$filterUserId}" : '';
$timestamp = date('Ymd-His');
$downloadFilename = "export-{$operationName}{$userSuffix}-{$timestamp}.xlsx";
// Envoyer le fichier Excel directement
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . $downloadFilename . '"');
header('Content-Length: ' . filesize($filepath));
header('Cache-Control: must-revalidate');
header('Pragma: public');
// Lire et envoyer le fichier
readfile($filepath);
LogService::log('Export Excel téléchargé', [
'level' => 'info',
'operationId' => $operationId,
'entiteId' => $entiteId,
'userId' => $userId,
'filename' => $downloadFilename,
'filterUserId' => $filterUserId
]);
exit;
} catch (Exception $e) {
LogService::log('Erreur lors de l\'export Excel', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la génération de l\'export Excel'
], 500);
}
}
/**
* Export JSON d'une opération (sauvegarde)
*/
public function exportJson(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
// Vérifier que l'opération existe et appartient à l'entité
$stmt = $this->db->prepare('
SELECT id, chk_active
FROM operations
WHERE id = ? AND fk_entite = ?
');
$stmt->execute([$operationId, $entiteId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
// Type d'export (manual par défaut)
$exportType = $_GET['type'] ?? 'manual';
// Générer l'export JSON
$exportService = new ExportService();
$fileInfo = $exportService->generateJsonExport($operationId, $entiteId, $exportType);
Response::json([
'status' => 'success',
'message' => 'Sauvegarde JSON générée avec succès',
'file' => $fileInfo
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de l\'export JSON', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la génération de la sauvegarde JSON'
], 500);
}
}
/**
* Export complet (Excel + JSON)
*/
public function exportFull(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
// Vérifier que l'opération existe et appartient à l'entité
$stmt = $this->db->prepare('
SELECT id, chk_active
FROM operations
WHERE id = ? AND fk_entite = ?
');
$stmt->execute([$operationId, $entiteId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
$exportService = new ExportService();
// Générer les deux exports
$excelFile = $exportService->generateExcelExport($operationId, $entiteId);
$jsonFile = $exportService->generateJsonExport($operationId, $entiteId, 'manual');
Response::json([
'status' => 'success',
'message' => 'Export complet généré avec succès',
'files' => [
'excel' => $excelFile,
'json' => $jsonFile
]
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de l\'export complet', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la génération de l\'export complet'
], 500);
}
}
/**
* Liste des sauvegardes d'une opération
*/
public function getBackups(string $id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
// Vérifier que l'opération existe et appartient à l'entité
$stmt = $this->db->prepare('
SELECT id
FROM operations
WHERE id = ? AND fk_entite = ?
');
$stmt->execute([$operationId, $entiteId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
Response::json([
'status' => 'error',
'message' => 'Opération non trouvée'
], 404);
return;
}
// Récupérer les fichiers d'export de cette opération
$stmt = $this->db->prepare('
SELECT
id, fichier, file_type, file_size, description,
created_at, fk_user_creat
FROM medias
WHERE support = "operation" AND support_id = ? AND fk_entite = ?
ORDER BY created_at DESC
');
$stmt->execute([$operationId, $entiteId]);
$backups = $stmt->fetchAll(PDO::FETCH_ASSOC);
Response::json([
'status' => 'success',
'backups' => $backups
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la récupération des sauvegardes', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la récupération des sauvegardes'
], 500);
}
}
/**
* Télécharger une sauvegarde spécifique
*/
public function downloadBackup(string $id, string $backup_id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
$backupId = (int)$backup_id;
// Vérifier que le fichier existe et appartient à l'opération/entité
$stmt = $this->db->prepare('
SELECT m.fichier, m.file_path, m.mime_type, m.original_name
FROM medias m
INNER JOIN operations o ON o.id = m.support_id
WHERE m.id = ? AND m.support = "operation" AND m.support_id = ?
AND o.fk_entite = ?
');
$stmt->execute([$backupId, $operationId, $entiteId]);
$backup = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$backup) {
Response::json([
'status' => 'error',
'message' => 'Fichier de sauvegarde non trouvé'
], 404);
return;
}
$filepath = getcwd() . '/' . $backup['file_path'];
if (!file_exists($filepath)) {
Response::json([
'status' => 'error',
'message' => 'Fichier physique non trouvé'
], 404);
return;
}
// Envoyer le fichier
header('Content-Type: ' . $backup['mime_type']);
header('Content-Disposition: attachment; filename="' . $backup['original_name'] . '"');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
} catch (Exception $e) {
LogService::log('Erreur lors du téléchargement de sauvegarde', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'backupId' => $backup_id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors du téléchargement'
], 500);
}
}
/**
* Supprimer une sauvegarde
*/
public function deleteBackup(string $id, string $backup_id): void {
try {
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = $this->getUserEntiteId($userId);
if (!$entiteId) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée pour cet utilisateur'
], 404);
return;
}
$operationId = (int)$id;
$backupId = (int)$backup_id;
// Vérifier que le fichier existe et appartient à l'opération/entité
$stmt = $this->db->prepare('
SELECT m.id, m.file_path
FROM medias m
INNER JOIN operations o ON o.id = m.support_id
WHERE m.id = ? AND m.support = "operation" AND m.support_id = ?
AND o.fk_entite = ?
');
$stmt->execute([$backupId, $operationId, $entiteId]);
$backup = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$backup) {
Response::json([
'status' => 'error',
'message' => 'Fichier de sauvegarde non trouvé'
], 404);
return;
}
// Supprimer le fichier physique
$filepath = getcwd() . '/' . $backup['file_path'];
if (file_exists($filepath)) {
unlink($filepath);
}
// Supprimer l'enregistrement en base
$stmt = $this->db->prepare('DELETE FROM medias WHERE id = ?');
$stmt->execute([$backupId]);
LogService::log('Suppression d\'une sauvegarde', [
'level' => 'info',
'userId' => $userId,
'operationId' => $operationId,
'backupId' => $backupId
]);
Response::json([
'status' => 'success',
'message' => 'Sauvegarde supprimée avec succès'
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la suppression de sauvegarde', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $id,
'backupId' => $backup_id,
'userId' => $userId ?? null
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la suppression'
], 500);
}
}
}