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