db = Database::getInstance(); $this->logService = new LogService(); $this->addressService = new AddressService(); $this->boundaryService = new DepartmentBoundaryService(); } /** * GET /sectors - Récupérer tous les secteurs */ public function index(): void { try { $entityId = $_SESSION['entity_id'] ?? null; if (!$entityId) { Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400); return; } $query = " SELECT s.id, s.libelle, s.color, s.sector, o.fk_entite, GROUP_CONCAT(ous.fk_user) as membres FROM ope_sectors s LEFT JOIN ope_users_sectors ous ON s.id = ous.fk_sector JOIN operations o ON s.fk_operation = o.id WHERE o.fk_entite = :entity_id GROUP BY s.id ORDER BY s.libelle "; $stmt = $this->db->prepare($query); $stmt->execute(['entity_id' => $entityId]); $sectors = $stmt->fetchAll(\PDO::FETCH_ASSOC); // Convertir les membres en tableau d'entiers foreach ($sectors as &$sector) { if ($sector['membres']) { $sector['membres'] = array_map('intval', explode(',', $sector['membres'])); } else { $sector['membres'] = []; } } Response::json(['status' => 'success', 'data' => $sectors]); } catch (\Exception $e) { $this->logService->error('Erreur lors de la récupération des secteurs', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la récupération des secteurs'], 500); } } /** * POST /sectors - Créer un nouveau secteur */ public function create(): void { try { $data = json_decode(file_get_contents('php://input'), true); // Validation des données - Structure directe depuis Flutter if (!isset($data['libelle']) || !isset($data['color']) || !isset($data['sector'])) { Response::json(['status' => 'error', 'message' => 'Données du secteur manquantes'], 400); return; } $sectorData = $data; $entityId = $data['fk_entite'] ?? $_SESSION['entity_id'] ?? null; $operationId = $data['operation_id'] ?? null; $userId = $data['user_id'] ?? $_SESSION['user_id'] ?? null; $users = $data['users'] ?? []; if (!$entityId) { Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400); return; } // Validation de l'opération if (!$operationId) { Response::json(['status' => 'error', 'message' => 'Opération non définie'], 400); return; } // Traitement des coordonnées pour validation $sector = $sectorData['sector']; $points = explode('#', rtrim($sector, '#')); $coordinates = []; foreach ($points as $point) { if (!empty($point)) { list($lat, $lng) = explode('/', $point); $coordinates[] = [floatval($lat), floatval($lng)]; // Format [lat, lng] pour AddressService } } if (count($coordinates) < 3) { Response::json(['status' => 'error', 'message' => 'Un secteur doit avoir au moins 3 points'], 400); return; } // Vérifier que le secteur est dans le département de l'entité try { // Récupérer le code postal de l'entité pour en déduire le département $deptQuery = "SELECT code_postal, encrypted_name FROM entites WHERE id = :entity_id"; $deptStmt = $this->db->prepare($deptQuery); $deptStmt->execute(['entity_id' => $entityId]); $entity = $deptStmt->fetch(); if (!$entity || !$entity['code_postal']) { Response::json(['status' => 'error', 'message' => 'Code postal de l\'entité non défini'], 400); return; } // Extraire le département du code postal (2 premiers caractères) $codePostal = $entity['code_postal']; if (strlen($codePostal) === 4) { $codePostal = '0' . $codePostal; // Ajouter le 0 devant si nécessaire } $departement = substr($codePostal, 0, 2); // Identifier tous les départements touchés par le secteur $departmentsTouched = $this->boundaryService->getDepartmentsForSector($coordinates); if (empty($departmentsTouched)) { $this->logService->warning('Aucun département trouvé pour le secteur', [ 'libelle' => $data['libelle'], 'entity_id' => $entityId, 'entity_dept' => $departement ]); } } catch (\Exception $e) { $this->logService->warning('Impossible de vérifier les limites départementales', [ 'error' => $e->getMessage(), 'libelle' => $data['libelle'] ]); } // Récupérer les adresses dans le polygone try { $addressCount = $this->addressService->countAddressesInPolygon($coordinates, $entityId); } catch (\Exception $e) { $this->logService->warning('Impossible de récupérer les adresses du secteur', [ 'error' => $e->getMessage(), 'libelle' => $data['libelle'], 'entity_id' => $entityId ]); $addressCount = 0; } $this->db->beginTransaction(); // Insertion du secteur $query = "INSERT INTO ope_sectors (libelle, color, sector, fk_operation) VALUES (:libelle, :color, :sector, :operation_id)"; $stmt = $this->db->prepare($query); $stmt->execute([ 'libelle' => $sectorData['libelle'], 'color' => $sectorData['color'], 'sector' => $sectorData['sector'], 'operation_id' => $operationId ]); $sectorId = $this->db->lastInsertId(); // Affectation des users si fournis if (!empty($users)) { $queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active) VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)"; $stmtMember = $this->db->prepare($queryMember); foreach ($users as $memberId) { $stmtMember->execute([ 'operation_id' => $operationId, 'user_id' => $memberId, 'sector_id' => $sectorId, 'user_creat' => $userId ]); } } // D'abord, chercher et intégrer les passages orphelins (fk_sector=NULL) dans le nouveau secteur $passagesIntegrated = 0; $addressesToExclude = []; // Adresses à ne pas créer en doublon try { // Créer le polygone SQL pour la vérification $polygonPoints = []; foreach ($coordinates as $coord) { $polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude } $polygonPoints[] = $polygonPoints[0]; // Fermer le polygone $polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))'; // Chercher les passages orphelins dans le périmètre du secteur $orphanQuery = "SELECT id, fk_adresse, gps_lat, gps_lng FROM ope_pass WHERE fk_operation = :operation_id AND fk_sector IS NULL AND gps_lat IS NOT NULL AND gps_lng IS NOT NULL AND ST_Contains(ST_GeomFromText(:polygon, 4326), POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))))"; $orphanStmt = $this->db->prepare($orphanQuery); $orphanStmt->execute([ 'operation_id' => $operationId, 'polygon' => $polygonString ]); $orphanPassages = $orphanStmt->fetchAll(); if (!empty($orphanPassages)) { $updateOrphanQuery = "UPDATE ope_pass SET fk_sector = :sector_id WHERE id = :passage_id"; $updateOrphanStmt = $this->db->prepare($updateOrphanQuery); foreach ($orphanPassages as $orphan) { $updateOrphanStmt->execute([ 'sector_id' => $sectorId, 'passage_id' => $orphan['id'] ]); $passagesIntegrated++; // Si le passage a un fk_adresse, l'ajouter à la liste des exclusions if (!empty($orphan['fk_adresse'])) { $addressesToExclude[] = $orphan['fk_adresse']; } } } } catch (\Exception $e) { $this->logService->warning('Erreur lors de la récupération des passages orphelins', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); } // Stocker les adresses du secteur $passagesCreated = 0; // Initialiser le compteur de passages try { $addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId); if (!empty($addresses)) { $queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng) VALUES (:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng)"; $stmtAddress = $this->db->prepare($queryAddress); foreach ($addresses as $address) { // Extraire le rue_bis si présent (généralement vide) $rueBis = ''; $stmtAddress->execute([ 'sector_id' => $sectorId, 'address_id' => $address['id'], 'numero' => $address['numero'], 'rue' => $address['voie'], 'rue_bis' => $rueBis, 'cp' => $address['code_postal'], 'ville' => $address['commune'], 'gps_lat' => $address['latitude'], 'gps_lng' => $address['longitude'] ]); } // Créer les passages pour chaque adresse if (!empty($users)) { $firstUserId = $users[0]; // Premier user pour l'affectation des passages $passageQuery = "INSERT INTO ope_pass ( fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active ) VALUES ( :operation_id, :sector_id, :user_id, :fk_adresse, :numero, :rue, :rue_bis, :ville, :gps_lat, :gps_lng, 2, '', NOW(), :user_creat, 1 )"; $passageStmt = $this->db->prepare($passageQuery); $passagesCreated = 0; foreach ($addresses as $address) { // Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin if (in_array($address['id'], $addressesToExclude)) { continue; // Passer à l'adresse suivante } try { // Extraire le rue_bis si présent (généralement vide) $rueBis = ''; $passageStmt->execute([ 'operation_id' => $operationId, 'sector_id' => $sectorId, 'user_id' => $firstUserId, 'fk_adresse' => $address['id'], 'numero' => $address['numero'], 'rue' => $address['voie'], 'rue_bis' => $rueBis, 'ville' => $address['commune'], 'gps_lat' => $address['latitude'], 'gps_lng' => $address['longitude'], 'user_creat' => $userId ]); $passagesCreated++; } catch (\Exception $e) { $this->logService->warning('Erreur lors de la création d\'un passage', [ 'address_id' => $address['id'], 'error' => $e->getMessage() ]); } } } } } catch (\Exception $e) { // En cas d'erreur avec les adresses, on ne bloque pas la création du secteur $this->logService->error('Erreur lors du stockage des adresses du secteur', [ 'sector_id' => $sectorId, 'error' => $e->getMessage(), 'entity_id' => $entityId ]); } $this->db->commit(); // Préparer les données de réponse $responseData = [ 'sector_id' => $sectorId ]; // Récupérer tous les passages du secteur (créés + intégrés) $totalPassages = (isset($passagesCreated) ? $passagesCreated : 0) + $passagesIntegrated; if ($totalPassages > 0) { $passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau, gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages FROM ope_pass WHERE fk_sector = :sector_id ORDER BY id"; $passagesStmt = $this->db->prepare($passagesQuery); $passagesStmt->execute(['sector_id' => $sectorId]); $passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC); // Déchiffrer les données sensibles des passages $passagesDecrypted = []; foreach ($passages as $passage) { // Déchiffrement du nom $passage['name'] = ''; if (!empty($passage['encrypted_name'])) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); } unset($passage['encrypted_name']); // Déchiffrement de l'email $passage['email'] = ''; if (!empty($passage['encrypted_email'])) { $decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']); if ($decryptedEmail) { $passage['email'] = $decryptedEmail; } } unset($passage['encrypted_email']); // Déchiffrement du téléphone $passage['phone'] = ''; if (!empty($passage['encrypted_phone'])) { $passage['phone'] = ApiService::decryptData($passage['encrypted_phone']); } unset($passage['encrypted_phone']); $passagesDecrypted[] = $passage; } $responseData['passages_sector'] = $passagesDecrypted; $responseData['passages_integrated'] = $passagesIntegrated; $responseData['passages_created'] = isset($passagesCreated) ? $passagesCreated : 0; } else { $responseData['passages_sector'] = []; $responseData['passages_integrated'] = 0; $responseData['passages_created'] = 0; } // Récupérer les users affectés $usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector FROM ope_users_sectors ous JOIN users u ON ous.fk_user = u.id WHERE ous.fk_sector = :sector_id"; $usersStmt = $this->db->prepare($usersQuery); $usersStmt->execute(['sector_id' => $sectorId]); $usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC); // Déchiffrer les noms des utilisateurs $responseData['users_sectors'] = []; foreach ($usersSectors as $userSector) { $userData = [ 'id' => $userSector['id'], 'first_name' => $userSector['first_name'] ?? '', 'sect_name' => $userSector['sect_name'] ?? '', 'fk_sector' => $userSector['fk_sector'], 'name' => '' ]; // Déchiffrer le nom if (!empty($userSector['encrypted_name'])) { $userData['name'] = ApiService::decryptData($userSector['encrypted_name']); } $responseData['users_sectors'][] = $userData; } $this->logService->info('Secteur créé', [ 'sector_id' => $sectorId, 'libelle' => $sectorData['libelle'], 'entity_id' => $entityId, 'user_id' => $userId, 'passages_created' => isset($passagesCreated) ? $passagesCreated : 0, 'passages_integrated' => $passagesIntegrated, 'users_assigned' => count($users) ]); // Construire la réponse selon la norme de l'API (sans groupe "data") $response = [ 'status' => 'success', 'message' => 'Secteur créé avec succès', 'sector' => [ 'id' => $sectorId, 'libelle' => $sectorData['libelle'], 'color' => $sectorData['color'], 'sector' => $sectorData['sector'] ] ]; // Ajouter les autres données directement à la racine if (isset($responseData['passages_sector'])) { $response['passages_sector'] = $responseData['passages_sector']; } if (isset($responseData['passages_integrated'])) { $response['passages_integrated'] = $responseData['passages_integrated']; } if (isset($responseData['passages_created'])) { $response['passages_created'] = $responseData['passages_created']; } if (isset($responseData['users_sectors'])) { $response['users_sectors'] = $responseData['users_sectors']; } Response::json($response, 201); } catch (\Exception $e) { if ($this->db->inTransaction()) { $this->db->rollBack(); } $this->logService->error('Erreur lors de la création du secteur', [ 'error' => $e->getMessage(), 'data' => $data ?? null ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la création du secteur'], 500); } } /** * PUT /sectors/{id} - Modifier un secteur */ public function update($id): void { try { $data = json_decode(file_get_contents('php://input'), true); $entityId = $_SESSION['entity_id'] ?? null; if (!$entityId) { Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400); return; } // Vérifier que le secteur appartient à l'entité $checkQuery = "SELECT s.id FROM ope_sectors s JOIN operations o ON s.fk_operation = o.id WHERE s.id = :id AND o.fk_entite = :entity_id"; $checkStmt = $this->db->prepare($checkQuery); $checkStmt->execute(['id' => $id, 'entity_id' => $entityId]); if (!$checkStmt->fetch()) { Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404); return; } $this->db->beginTransaction(); // Mise à jour du secteur $updateFields = []; $params = ['id' => $id]; if (isset($data['libelle'])) { $updateFields[] = 'libelle = :libelle'; $params['libelle'] = $data['libelle']; } if (isset($data['color'])) { $updateFields[] = 'color = :color'; $params['color'] = $data['color']; } if (isset($data['sector'])) { $updateFields[] = 'sector = :sector'; $params['sector'] = $data['sector']; } if (!empty($updateFields)) { $query = "UPDATE ope_sectors SET " . implode(', ', $updateFields) . " WHERE id = :id"; $stmt = $this->db->prepare($query); $stmt->execute($params); } // Gestion des membres if (isset($data['membres'])) { // Supprimer les affectations existantes $deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id"; $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute(['sector_id' => $id]); // Ajouter les nouvelles affectations if (!empty($data['membres'])) { $insertQuery = "INSERT INTO ope_users_sectors (fk_user, fk_sector) VALUES (:user_id, :sector_id)"; $insertStmt = $this->db->prepare($insertQuery); foreach ($data['membres'] as $memberId) { $insertStmt->execute([ 'user_id' => $memberId, 'sector_id' => $id ]); } } } // Gérer les passages si le secteur a changé $passageCounters = [ 'passages_orphaned' => 0, 'passages_updated' => 0, 'passages_created' => 0, 'passages_kept' => 0 ]; if (isset($data['sector'])) { // Mettre à jour les adresses du secteur AVANT de traiter les passages try { // Supprimer les anciennes adresses $deleteAddressQuery = "DELETE FROM sectors_adresses WHERE fk_sector = :sector_id"; $deleteAddressStmt = $this->db->prepare($deleteAddressQuery); $deleteAddressStmt->execute(['sector_id' => $id]); // Traiter les nouvelles coordonnées $points = explode('#', rtrim($data['sector'], '#')); $coordinates = []; foreach ($points as $point) { if (!empty($point)) { list($lat, $lng) = explode('/', $point); $coordinates[] = [floatval($lat), floatval($lng)]; } } // Récupérer et stocker les nouvelles adresses $this->logService->info('[UPDATE] Récupération des adresses', [ 'sector_id' => $id, 'entity_id' => $entityId, 'nb_points' => count($coordinates) ]); $addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId); $this->logService->info('[UPDATE] Adresses récupérées', [ 'sector_id' => $id, 'nb_addresses' => count($addresses) ]); if (!empty($addresses)) { $queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng) VALUES (:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng)"; $stmtAddress = $this->db->prepare($queryAddress); foreach ($addresses as $address) { $stmtAddress->execute([ 'sector_id' => $id, 'address_id' => $address['id'], 'numero' => $address['numero'], 'rue' => $address['voie'], 'cp' => $address['code_postal'], 'ville' => $address['commune'], 'gps_lat' => $address['latitude'], 'gps_lng' => $address['longitude'] ]); } $this->logService->info('[UPDATE] Adresses stockées dans sectors_adresses', [ 'sector_id' => $id, 'nb_stored' => count($addresses) ]); } else { $this->logService->warning('[UPDATE] Aucune adresse trouvée pour le secteur', [ 'sector_id' => $id, 'entity_id' => $entityId ]); } // Vérifier si c'est un problème de connexion à la base d'adresses if (!$this->addressService->isConnected()) { $this->logService->warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [ 'sector_id' => $id ]); } } catch (\Exception $e) { $this->logService->error('[UPDATE] Erreur lors de la mise à jour des adresses du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); } // Maintenant que les adresses sont mises à jour, traiter les passages $this->logService->info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]); $passageCounters = $this->updatePassagesForSector($id, $data['sector']); } $this->db->commit(); // Récupérer le secteur mis à jour $query = " SELECT s.id, s.libelle, s.color, s.sector FROM ope_sectors s WHERE s.id = :id "; $stmt = $this->db->prepare($query); $stmt->execute(['id' => $id]); $sector = $stmt->fetch(\PDO::FETCH_ASSOC); // Récupérer tous les passages du secteur $passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau, gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages FROM ope_pass WHERE fk_sector = :sector_id ORDER BY id"; $passagesStmt = $this->db->prepare($passagesQuery); $passagesStmt->execute(['sector_id' => $id]); $passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC); // Déchiffrer les données sensibles des passages $passagesDecrypted = []; foreach ($passages as $passage) { // Déchiffrement du nom $passage['name'] = ''; if (!empty($passage['encrypted_name'])) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); } unset($passage['encrypted_name']); // Déchiffrement de l'email $passage['email'] = ''; if (!empty($passage['encrypted_email'])) { $decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']); if ($decryptedEmail) { $passage['email'] = $decryptedEmail; } } unset($passage['encrypted_email']); // Déchiffrement du téléphone $passage['phone'] = ''; if (!empty($passage['encrypted_phone'])) { $passage['phone'] = ApiService::decryptData($passage['encrypted_phone']); } unset($passage['encrypted_phone']); $passagesDecrypted[] = $passage; } // Récupérer les users affectés $usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector FROM ope_users_sectors ous JOIN users u ON ous.fk_user = u.id WHERE ous.fk_sector = :sector_id"; $usersStmt = $this->db->prepare($usersQuery); $usersStmt->execute(['sector_id' => $id]); $usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC); // Déchiffrer les noms des utilisateurs $usersDecrypted = []; foreach ($usersSectors as $userSector) { $userData = [ 'id' => $userSector['id'], 'first_name' => $userSector['first_name'] ?? '', 'sect_name' => $userSector['sect_name'] ?? '', 'fk_sector' => $userSector['fk_sector'], 'name' => '' ]; // Déchiffrer le nom if (!empty($userSector['encrypted_name'])) { $userData['name'] = ApiService::decryptData($userSector['encrypted_name']); } $usersDecrypted[] = $userData; } $this->logService->info('Secteur modifié', [ 'sector_id' => $id, 'updates' => array_keys($data), 'passage_counters' => $passageCounters, 'user_id' => $_SESSION['user_id'] ?? null ]); // Construire la réponse identique à create avec les compteurs supplémentaires $response = [ 'status' => 'success', 'message' => 'Secteur modifié avec succès', 'sector' => $sector, 'passages_sector' => $passagesDecrypted, 'passages_orphaned' => $passageCounters['passages_orphaned'], 'passages_deleted' => $passageCounters['passages_deleted'], 'passages_updated' => $passageCounters['passages_updated'], 'passages_created' => $passageCounters['passages_created'], 'passages_total' => count($passagesDecrypted), 'users_sectors' => $usersDecrypted ]; Response::json($response); } catch (\Exception $e) { // Vérifier si une transaction est active avant de faire un rollback if ($this->db->inTransaction()) { $this->db->rollBack(); } $this->logService->error('Erreur lors de la modification du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la modification du secteur'], 500); } } /** * GET /sectors/{id}/addresses - Récupérer les adresses d'un secteur */ public function getAddresses($id): void { try { $entityId = $_SESSION['entity_id'] ?? null; if (!$entityId) { Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400); return; } // Vérifier que le secteur appartient à l'entité $checkQuery = "SELECT s.id FROM ope_sectors s JOIN operations o ON s.fk_operation = o.id WHERE s.id = :id AND o.fk_entite = :entity_id"; $checkStmt = $this->db->prepare($checkQuery); $checkStmt->execute(['id' => $id, 'entity_id' => $entityId]); if (!$checkStmt->fetch()) { Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404); return; } // Récupérer les adresses du secteur $query = " SELECT fk_address as id, numero, voie, code_postal, commune, latitude, longitude FROM sectors_adresses WHERE fk_sector = :sector_id ORDER BY commune, voie, CAST(numero AS UNSIGNED) "; $stmt = $this->db->prepare($query); $stmt->execute(['sector_id' => $id]); $addresses = $stmt->fetchAll(\PDO::FETCH_ASSOC); // Compter le total $countQuery = "SELECT COUNT(*) as total FROM sectors_adresses WHERE fk_sector = :sector_id"; $countStmt = $this->db->prepare($countQuery); $countStmt->execute(['sector_id' => $id]); $total = $countStmt->fetch()['total']; Response::json([ 'status' => 'success', 'data' => $addresses, 'total' => $total ]); } catch (\Exception $e) { $this->logService->error('Erreur lors de la récupération des adresses du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la récupération des adresses'], 500); } } /** * DELETE /sectors/{id} - Supprimer un secteur */ public function delete($id): void { try { // Récupérer les données de la requête si présentes (pour les API stateless) $data = json_decode(file_get_contents('php://input'), true) ?? []; $entityId = $data['fk_entite'] ?? $_SESSION['entity_id'] ?? null; if (!$entityId) { Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400); return; } // Vérifier que le secteur existe et récupérer ses informations $checkQuery = "SELECT s.id, s.libelle, o.fk_entite FROM ope_sectors s JOIN operations o ON s.fk_operation = o.id WHERE s.id = :id"; $checkStmt = $this->db->prepare($checkQuery); $checkStmt->execute(['id' => $id]); $sector = $checkStmt->fetch(); if (!$sector || $sector['fk_entite'] != $entityId) { Response::json(['status' => 'error', 'message' => 'Secteur non trouvé ou non autorisé'], 404); return; } $this->db->beginTransaction(); // Compter les passages à supprimer (fk_type=2 et encrypted_name vide) $countDeleteQuery = "SELECT COUNT(*) as count FROM ope_pass WHERE fk_sector = :sector_id AND fk_type = 2 AND (encrypted_name IS NULL OR encrypted_name = '')"; $countDeleteStmt = $this->db->prepare($countDeleteQuery); $countDeleteStmt->execute(['sector_id' => $id]); $passagesToDelete = $countDeleteStmt->fetch()['count']; // Récupérer les passages à réaffecter avant de les modifier $getPassagesToUpdateQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau, gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages FROM ope_pass WHERE fk_sector = :sector_id AND NOT (fk_type = 2 AND (encrypted_name IS NULL OR encrypted_name = ''))"; $getPassagesToUpdateStmt = $this->db->prepare($getPassagesToUpdateQuery); $getPassagesToUpdateStmt->execute(['sector_id' => $id]); $passagesToUpdate = $getPassagesToUpdateStmt->fetchAll(\PDO::FETCH_ASSOC); // Supprimer les passages avec fk_type=2 et encrypted_name vide $deletePassagesQuery = "DELETE FROM ope_pass WHERE fk_sector = :sector_id AND fk_type = 2 AND (encrypted_name IS NULL OR encrypted_name = '')"; $deletePassagesStmt = $this->db->prepare($deletePassagesQuery); $deletePassagesStmt->execute(['sector_id' => $id]); // Réaffecter les autres passages au secteur NULL $updatePassagesQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE fk_sector = :sector_id AND NOT (fk_type = 2 AND (encrypted_name IS NULL OR encrypted_name = ''))"; $updatePassagesStmt = $this->db->prepare($updatePassagesQuery); $updatePassagesStmt->execute(['sector_id' => $id]); // Supprimer les affectations de membres $deleteMembersQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id"; $deleteMembersStmt = $this->db->prepare($deleteMembersQuery); $deleteMembersStmt->execute(['sector_id' => $id]); // Supprimer les adresses associées au secteur $deleteAddressesQuery = "DELETE FROM sectors_adresses WHERE fk_sector = :sector_id"; $deleteAddressesStmt = $this->db->prepare($deleteAddressesQuery); $deleteAddressesStmt->execute(['sector_id' => $id]); // Supprimer le secteur $deleteQuery = "DELETE FROM ope_sectors WHERE id = :id"; $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute(['id' => $id]); $this->db->commit(); // Déchiffrer les données sensibles des passages $passagesDecrypted = []; foreach ($passagesToUpdate as $passage) { // Déchiffrement du nom $passage['name'] = ''; if (!empty($passage['encrypted_name'])) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); } unset($passage['encrypted_name']); // Déchiffrement de l'email $passage['email'] = ''; if (!empty($passage['encrypted_email'])) { $decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']); if ($decryptedEmail) { $passage['email'] = $decryptedEmail; } } unset($passage['encrypted_email']); // Déchiffrement du téléphone $passage['phone'] = ''; if (!empty($passage['encrypted_phone'])) { $passage['phone'] = ApiService::decryptData($passage['encrypted_phone']); } unset($passage['encrypted_phone']); $passagesDecrypted[] = $passage; } $this->logService->info('Secteur supprimé', [ 'sector_id' => $id, 'libelle' => $sector['libelle'], 'passages_deleted' => $passagesToDelete, 'passages_reassigned' => count($passagesToUpdate), 'user_id' => $_SESSION['user_id'] ?? null ]); Response::json([ 'status' => 'success', 'message' => 'Secteur supprimé avec succès', 'passages_deleted' => $passagesToDelete, 'passages_reassigned' => count($passagesToUpdate), 'passages_sector' => $passagesDecrypted ]); } catch (\Exception $e) { $this->db->rollBack(); $this->logService->error('Erreur lors de la suppression du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la suppression du secteur'], 500); } } /** * GET /sectors/department-boundaries/status - Vérifier le statut des contours départementaux */ public function departmentBoundariesStatus(): void { try { $status = $this->boundaryService->checkDepartmentContoursStatus(); Response::json([ 'status' => 'success', 'data' => $status ]); } catch (\Exception $e) { $this->logService->error('Erreur lors de la vérification des contours départementaux', [ 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500); } } /** * POST /sectors/check-boundaries - Vérifier si un secteur respecte les limites départementales */ public function checkBoundaries(): void { try { $data = json_decode(file_get_contents('php://input'), true); if (!isset($data['sector']) || !isset($data['entity_id'])) { Response::json(['status' => 'error', 'message' => 'Données manquantes'], 400); return; } // Traiter les coordonnées $sector = $data['sector']; $points = explode('#', rtrim($sector, '#')); $coordinates = []; foreach ($points as $point) { if (!empty($point)) { list($lat, $lng) = explode('/', $point); $coordinates[] = [floatval($lat), floatval($lng)]; } } // Récupérer le département de l'entité $deptQuery = "SELECT departement, nom FROM entites WHERE id = :entity_id"; $deptStmt = $this->db->prepare($deptQuery); $deptStmt->execute(['entity_id' => $data['entity_id']]); $entity = $deptStmt->fetch(); if (!$entity || !$entity['departement']) { Response::json(['status' => 'error', 'message' => 'Département de l\'entité non défini'], 400); return; } // Vérifier les limites $boundaryCheck = $this->boundaryService->checkSectorInDepartment($coordinates, $entity['departement']); // Récupérer aussi la liste de tous les départements touchés $intersectingDepts = $this->boundaryService->getDepartmentsForSector($coordinates); Response::json([ 'status' => 'success', 'data' => [ 'is_valid' => $boundaryCheck['is_contained'], 'entity_department' => $entity['departement'], 'message' => $boundaryCheck['message'], 'departments' => $intersectingDepts ] ]); } catch (\Exception $e) { $this->logService->error('Erreur lors de la vérification des limites', [ 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500); } } /** * Mettre à jour les passages affectés à un secteur lors de la modification du périmètre * Retourne un tableau avec les compteurs détaillés */ private function updatePassagesForSector($sectorId, $newSectorCoords): array { $counters = [ 'passages_orphaned' => 0, 'passages_deleted' => 0, 'passages_updated' => 0, 'passages_created' => 0, 'passages_kept' => 0 ]; try { // Récupérer l'opération et l'entité du secteur $sectorQuery = "SELECT o.id as operation_id, o.fk_entite, s.fk_operation FROM ope_sectors s JOIN operations o ON s.fk_operation = o.id WHERE s.id = :sector_id"; $sectorStmt = $this->db->prepare($sectorQuery); $sectorStmt->execute(['sector_id' => $sectorId]); $sectorInfo = $sectorStmt->fetch(); if (!$sectorInfo) { return 0; } $operationId = $sectorInfo['operation_id']; $entityId = $sectorInfo['fk_entite']; // Traiter les coordonnées pour créer le polygone $points = explode('#', rtrim($newSectorCoords, '#')); $coordinates = []; $polygonPoints = []; foreach ($points as $point) { if (!empty($point)) { list($lat, $lng) = explode('/', $point); $coordinates[] = [floatval($lat), floatval($lng)]; $polygonPoints[] = floatval($lng) . ' ' . floatval($lat); // longitude latitude pour SQL } } $polygonPoints[] = $polygonPoints[0]; // Fermer le polygone $polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))'; // 1. VÉRIFICATION GÉOGRAPHIQUE DES PASSAGES EXISTANTS $checkPassagesQuery = "SELECT id, gps_lat, gps_lng, fk_type, encrypted_name FROM ope_pass WHERE fk_sector = :sector_id AND gps_lat IS NOT NULL AND gps_lng IS NOT NULL"; $checkStmt = $this->db->prepare($checkPassagesQuery); $checkStmt->execute(['sector_id' => $sectorId]); $existingPassages = $checkStmt->fetchAll(); $passagesToDelete = []; foreach ($existingPassages as $passage) { // Vérifier si le passage est dans le nouveau polygone $pointInPolygonQuery = "SELECT ST_Contains(ST_GeomFromText(:polygon, 4326), POINT(CAST(:lng AS DECIMAL(10,8)), CAST(:lat AS DECIMAL(10,8)))) as is_inside"; $pointStmt = $this->db->prepare($pointInPolygonQuery); $pointStmt->execute([ 'polygon' => $polygonString, 'lng' => $passage['gps_lng'], 'lat' => $passage['gps_lat'] ]); $result = $pointStmt->fetch(); if ($result['is_inside'] == 0) { // Le passage est hors du nouveau périmètre // Vérifier si c'est un passage non visité (fk_type=2 ET encrypted_name vide) if ($passage['fk_type'] == 2 && ($passage['encrypted_name'] === '' || $passage['encrypted_name'] === null)) { // Passage non visité : à supprimer $passagesToDelete[] = $passage['id']; $counters['passages_deleted'] = ($counters['passages_deleted'] ?? 0) + 1; } else { // Passage visité : mettre en orphelin $orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id = :passage_id"; $orphanStmt = $this->db->prepare($orphanQuery); $orphanStmt->execute(['passage_id' => $passage['id']]); $counters['passages_orphaned']++; } } else { $counters['passages_kept']++; } } // Supprimer les passages non visités qui sont hors zone if (!empty($passagesToDelete)) { $deleteQuery = "DELETE FROM ope_pass WHERE id IN (" . implode(',', $passagesToDelete) . ")"; $this->db->exec($deleteQuery); } // 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES // Récupérer toutes les adresses du secteur depuis sectors_adresses $addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id"; $addressesStmt = $this->db->prepare($addressesQuery); $addressesStmt->execute(['sector_id' => $sectorId]); $addresses = $addressesStmt->fetchAll(); $this->logService->info('[updatePassagesForSector] Adresses dans sectors_adresses', [ 'sector_id' => $sectorId, 'nb_addresses' => count($addresses) ]); // Récupérer le premier utilisateur affecté au secteur $userQuery = "SELECT fk_user FROM ope_users_sectors WHERE fk_sector = :sector_id LIMIT 1"; $userStmt = $this->db->prepare($userQuery); $userStmt->execute(['sector_id' => $sectorId]); $firstUser = $userStmt->fetch(); $firstUserId = $firstUser ? $firstUser['fk_user'] : null; if ($firstUserId && !empty($addresses)) { $this->logService->info('[updatePassagesForSector] Création passages pour user', [ 'user_id' => $firstUserId, 'nb_addresses_to_process' => count($addresses) ]); // Préparer la requête de création de passage (même format que dans create) $createPassageQuery = "INSERT INTO ope_pass ( fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active ) VALUES ( :operation_id, :sector_id, :user_id, :fk_adresse, :numero, :rue, :rue_bis, :ville, :gps_lat, :gps_lng, 2, '', NOW(), :user_creat, 1 )"; $createStmt = $this->db->prepare($createPassageQuery); foreach ($addresses as $address) { // 2.1 Vérification primaire par fk_adresse if (!empty($address['fk_adresse'])) { $checkByAddressQuery = "SELECT id FROM ope_pass WHERE fk_operation = :operation_id AND fk_adresse = :fk_adresse"; $checkByAddressStmt = $this->db->prepare($checkByAddressQuery); $checkByAddressStmt->execute([ 'operation_id' => $operationId, 'fk_adresse' => $address['fk_adresse'] ]); if ($checkByAddressStmt->fetch()) { continue; // Passage déjà existant, passer au suivant } } // 2.2 Vérification secondaire par données d'adresse $checkByDataQuery = "SELECT id FROM ope_pass WHERE fk_operation = :operation_id AND numero = :numero AND rue_bis = :rue_bis AND rue = :rue AND ville = :ville"; $checkByDataStmt = $this->db->prepare($checkByDataQuery); $checkByDataStmt->execute([ 'operation_id' => $operationId, 'numero' => $address['numero'], 'rue_bis' => $address['rue_bis'], 'rue' => $address['rue'], 'ville' => $address['ville'] ]); $matchingPassages = $checkByDataStmt->fetchAll(); if (!empty($matchingPassages)) { // Mettre à jour les passages trouvés avec le fk_adresse if (!empty($address['fk_adresse'])) { $updateQuery = "UPDATE ope_pass SET fk_adresse = :fk_adresse WHERE id = :passage_id"; $updateStmt = $this->db->prepare($updateQuery); foreach ($matchingPassages as $matchingPassage) { $updateStmt->execute([ 'fk_adresse' => $address['fk_adresse'], 'passage_id' => $matchingPassage['id'] ]); $counters['passages_updated']++; } } continue; } // 2.3 Création du passage (aucun passage existant trouvé) try { $createStmt->execute([ 'operation_id' => $operationId, 'sector_id' => $sectorId, 'user_id' => $firstUserId, 'fk_adresse' => $address['fk_adresse'], 'numero' => $address['numero'], 'rue' => $address['rue'], 'rue_bis' => $address['rue_bis'], 'ville' => $address['ville'], 'gps_lat' => $address['gps_lat'], 'gps_lng' => $address['gps_lng'], 'user_creat' => $_SESSION['user_id'] ?? null ]); $counters['passages_created']++; } catch (\Exception $e) { $this->logService->warning('Erreur lors de la création d\'un passage pendant update', [ 'sector_id' => $sectorId, 'address' => $address, 'error' => $e->getMessage() ]); } } } else { $this->logService->warning('[updatePassagesForSector] Pas de création de passages', [ 'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses', 'first_user_id' => $firstUserId, 'nb_addresses' => count($addresses) ]); } // Retourner les compteurs détaillés $this->logService->info('[updatePassagesForSector] Fin traitement', [ 'sector_id' => $sectorId, 'counters' => $counters ]); return $counters; } catch (\Exception $e) { $this->logService->error('Erreur lors de la mise à jour des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); return [ 'passages_orphaned' => 0, 'passages_deleted' => 0, 'passages_updated' => 0, 'passages_created' => 0, 'passages_kept' => 0 ]; } } }