db = Database::getInstance(); $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) { 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)) { LogService::warning('Aucun département trouvé pour le secteur', [ 'libelle' => $data['libelle'], 'entity_id' => $entityId, 'entity_dept' => $departement ]); } } catch (\Exception $e) { 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) { 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, updated_at, fk_user_creat, chk_active) VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)"; $stmtMember = $this->db->prepare($queryMember); foreach ($users as $memberId) { // $memberId est DÉJÀ ope_users.id (envoyé par Flutter) // Vérifier que cet ope_users.id existe et appartient bien à l'opération $stmtOpeUser = $this->db->prepare(' SELECT id FROM ope_users WHERE id = ? AND fk_operation = ? '); $stmtOpeUser->execute([$memberId, $operationId]); $opeUserId = $stmtOpeUser->fetchColumn(); if (!$opeUserId) { LogService::warning('ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $memberId, 'operation_id' => $operationId ]); continue; } $stmtMember->execute([ 'operation_id' => $operationId, 'user_id' => $opeUserId, '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) { 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); // Enrichir les adresses avec les données bâtiments $addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId); if (!empty($addresses)) { $queryAddress = "INSERT INTO sectors_adresses ( fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng, fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol ) VALUES ( :sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng, :fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol )"; $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'], 'fk_batiment' => $address['fk_batiment'] ?? null, 'fk_habitat' => $address['fk_habitat'] ?? 1, 'nb_niveau' => $address['nb_niveau'] ?? null, 'nb_log' => $address['nb_log'] ?? null, 'residence' => $address['residence'] ?? '', 'alt_sol' => $address['alt_sol'] ?? null ]); } // Créer les passages pour chaque adresse if (!empty($users)) { // Récupérer ope_users.id pour le premier utilisateur // $users[0] est DÉJÀ ope_users.id (envoyé par Flutter) $stmtFirstOpeUser = $this->db->prepare(' SELECT id FROM ope_users WHERE id = ? AND fk_operation = ? '); $stmtFirstOpeUser->execute([$users[0], $operationId]); $firstOpeUserId = $stmtFirstOpeUser->fetchColumn(); if (!$firstOpeUserId) { LogService::warning('Premier ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $users[0], 'operation_id' => $operationId ]); // Pas de création de passages sans utilisateur valide dans ope_users } else { $passageQuery = "INSERT INTO ope_pass ( fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville, residence, appt, fk_habitat, gps_lat, gps_lng, fk_type, nb_passages, encrypted_name, created_at, fk_user_creat, chk_active ) VALUES ( :operation_id, :sector_id, :user_id, :fk_adresse, :numero, :rue, :rue_bis, :ville, :residence, :appt, :fk_habitat, :gps_lat, :gps_lng, 2, 0, '', 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 = ''; // Déterminer le nombre de passages à créer $fkHabitat = $address['fk_habitat'] ?? 1; $nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1; $residence = $address['residence'] ?? ''; // IMPORTANT : Uniformisation GPS pour les immeubles // Tous les passages d'une même adresse partagent les mêmes coordonnées GPS // Issues de la table adresses enrichie (gps_lat, gps_lng) $gpsLat = $address['latitude']; $gpsLng = $address['longitude']; // Créer 1 passage pour maison individuelle, nb_log passages pour immeuble for ($i = 1; $i <= $nbLog; $i++) { $appt = ($fkHabitat == 2) ? (string)$i : ''; // Numéro d'appartement pour immeubles $passageStmt->execute([ 'operation_id' => $operationId, 'sector_id' => $sectorId, 'user_id' => $firstOpeUserId, 'fk_adresse' => $address['id'], 'numero' => $address['numero'], 'rue' => $address['voie'], 'rue_bis' => $rueBis, 'ville' => $address['commune'], 'residence' => $residence, 'appt' => $appt, 'fk_habitat' => $fkHabitat, 'gps_lat' => $gpsLat, 'gps_lng' => $gpsLng, 'user_creat' => $userId ]); $passagesCreated++; } // Log pour vérifier l'uniformisation GPS (surtout pour immeubles) if ($fkHabitat == 2 && $nbLog > 1) { LogService::info('[SectorController] Création passages immeuble avec GPS uniformisés', [ 'address_id' => $address['id'], 'nb_passages' => $nbLog, 'gps_lat' => $gpsLat, 'gps_lng' => $gpsLng, 'residence' => $residence ]); } } catch (\Exception $e) { 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 LogService::error('Erreur lors du stockage des adresses du secteur', [ 'sector_id' => $sectorId, 'error' => $e->getMessage(), 'entity_id' => $entityId ]); } $this->db->commit(); // Log de création du secteur EventLogService::logSectorCreated( (int)$sectorId, (int)$operationId, $sectorData['libelle'] ); // 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, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector FROM ope_users_sectors ous JOIN ope_users ou ON ous.fk_user = ou.id JOIN users u ON ou.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 = [ 'user_id' => $userSector['id'], 'ope_user_id' => $userSector['ope_user_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; } 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(); } 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, s.fk_operation, s.libelle 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]); $existingSector = $checkStmt->fetch(); if (!$existingSector) { Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404); return; } $operationId = $existingSector['fk_operation']; $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 (reçus comme 'users' depuis Flutter) if (isset($data['users'])) { LogService::info('[UPDATE USERS] Début modification des membres', [ 'sector_id' => $id, 'users_demandes' => $data['users'], 'nb_users' => count($data['users']) ]); // Récupérer l'opération du secteur pour l'INSERT $opQuery = "SELECT fk_operation FROM ope_sectors WHERE id = :sector_id"; LogService::info('[UPDATE USERS] SQL - Récupération fk_operation', [ 'query' => $opQuery, 'params' => ['sector_id' => $id] ]); $opStmt = $this->db->prepare($opQuery); $opStmt->execute(['sector_id' => $id]); $operationId = $opStmt->fetch()['fk_operation']; LogService::info('[UPDATE USERS] fk_operation récupéré', [ 'operation_id' => $operationId ]); // Supprimer les affectations existantes $deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id"; LogService::info('[UPDATE USERS] SQL - Suppression des anciens membres', [ 'query' => $deleteQuery, 'params' => ['sector_id' => $id] ]); $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute(['sector_id' => $id]); $deletedCount = $deleteStmt->rowCount(); LogService::info('[UPDATE USERS] Membres supprimés', [ 'nb_deleted' => $deletedCount ]); // Ajouter les nouvelles affectations if (!empty($data['users'])) { $insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active) VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)"; LogService::info('[UPDATE USERS] SQL - Requête INSERT préparée', [ 'query' => $insertQuery ]); $insertStmt = $this->db->prepare($insertQuery); $insertedUsers = []; $failedUsers = []; foreach ($data['users'] as $memberId) { try { // $memberId est DÉJÀ ope_users.id (envoyé par Flutter) // Vérifier que cet ope_users.id existe et appartient bien à l'opération $stmtOpeUser = $this->db->prepare(' SELECT id FROM ope_users WHERE id = ? AND fk_operation = ? '); $stmtOpeUser->execute([$memberId, $operationId]); $opeUserId = $stmtOpeUser->fetchColumn(); if (!$opeUserId) { LogService::warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $memberId, 'operation_id' => $operationId ]); $failedUsers[] = $memberId; continue; } $params = [ 'operation_id' => $operationId, 'user_id' => $opeUserId, 'sector_id' => $id, 'user_creat' => $_SESSION['user_id'] ?? null ]; LogService::info('[UPDATE USERS] SQL - INSERT user', [ 'params' => $params ]); $insertStmt->execute($params); $insertedUsers[] = $memberId; LogService::info('[UPDATE USERS] User inséré avec succès', [ 'user_id' => $memberId ]); } catch (\PDOException $e) { $failedUsers[] = $memberId; LogService::warning('[UPDATE USERS] ERREUR insertion user', [ 'sector_id' => $id, 'user_id' => $memberId, 'error' => $e->getMessage(), 'error_code' => $e->getCode() ]); } } LogService::info('[UPDATE USERS] Résultat des insertions', [ 'users_demandes' => $data['users'], 'users_inseres' => $insertedUsers, 'users_echoues' => $failedUsers, 'nb_succes' => count($insertedUsers), 'nb_echecs' => count($failedUsers) ]); } } // Gérer les passages si le secteur a changé ET si chk_adresses_change = 1 $passageCounters = [ 'passages_orphaned' => 0, 'passages_updated' => 0, 'passages_created' => 0, 'passages_kept' => 0 ]; // chk_adresses_change : 0=ne pas toucher aux adresses/passages, 1=recalculer (défaut) $chkAdressesChange = $data['chk_adresses_change'] ?? 1; if (isset($data['sector']) && $chkAdressesChange == 0) { LogService::info('[UPDATE] Modification secteur sans recalcul adresses/passages', [ 'sector_id' => $id, 'chk_adresses_change' => $chkAdressesChange ]); } if (isset($data['sector']) && $chkAdressesChange == 1) { // 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 LogService::info('[UPDATE] Récupération des adresses', [ 'sector_id' => $id, 'entity_id' => $entityId, 'nb_points' => count($coordinates) ]); $addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId); // Enrichir les adresses avec les données bâtiments $addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId); 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, fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol ) VALUES ( :sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng, :fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol )"; $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'], 'fk_batiment' => $address['fk_batiment'] ?? null, 'fk_habitat' => $address['fk_habitat'] ?? 1, 'nb_niveau' => $address['nb_niveau'] ?? null, 'nb_log' => $address['nb_log'] ?? null, 'residence' => $address['residence'] ?? '', 'alt_sol' => $address['alt_sol'] ?? null ]); } LogService::info('[UPDATE] Adresses stockées dans sectors_adresses', [ 'sector_id' => $id, 'nb_stored' => count($addresses) ]); } else { 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()) { LogService::warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [ 'sector_id' => $id ]); } } catch (\Exception $e) { 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 LogService::info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]); $passageCounters = $this->updatePassagesForSector($id, $data['sector']); } // Commit des modifications (users et/ou secteur) $this->db->commit(); // Log de mise à jour du secteur $changes = []; if (isset($data['libelle'])) { $changes['libelle'] = ['new' => $data['libelle']]; } if (isset($data['color'])) { $changes['color'] = ['new' => $data['color']]; } if (isset($data['sector'])) { $changes['sector'] = true; // Polygon modifié } if (isset($data['users'])) { $changes['users'] = true; // Affectation modifiée } if (!empty($changes)) { EventLogService::logSectorUpdated((int)$id, (int)$operationId, $changes); } // 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 les passages UNIQUEMENT si chk_adresses_change = 1 $passagesDecrypted = []; if ($chkAdressesChange == 1) { // 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 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 (avec READ UNCOMMITTED pour forcer la lecture des données fraîches) $usersQuery = "SELECT u.id, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector FROM ope_users_sectors ous JOIN ope_users ou ON ous.fk_user = ou.id JOIN users u ON ou.fk_user = u.id WHERE ous.fk_sector = :sector_id ORDER BY u.id"; LogService::info('[UPDATE USERS] SQL - Récupération finale des users', [ 'query' => $usersQuery, 'params' => ['sector_id' => $id] ]); $usersStmt = $this->db->prepare($usersQuery); $usersStmt->execute(['sector_id' => $id]); $usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC); $userIds = array_column($usersSectors, 'id'); LogService::info('[UPDATE USERS] Users récupérés après commit', [ 'sector_id' => $id, 'users_ids' => $userIds, 'nb_users' => count($userIds), 'users_demandes_initialement' => $data['users'] ?? [] ]); // Déchiffrer les noms des utilisateurs $usersDecrypted = []; foreach ($usersSectors as $userSector) { $userData = [ 'user_id' => $userSector['id'], 'ope_user_id' => $userSector['ope_user_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; } 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(); } 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) { 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, s.fk_operation, 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; } $operationId = $sector['fk_operation']; $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(); // Log de suppression du secteur (suppression physique = false) EventLogService::logSectorDeleted( (int)$id, (int)$operationId, false // suppression physique (DELETE) ); // 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; } 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(); 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) { 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) { 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 * VERSION OPTIMISÉE avec requêtes groupées * 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 $counters; } $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 (OPTIMISÉE) // Utiliser une seule requête pour vérifier tous les passages $checkPassagesQuery = " SELECT p.id, p.gps_lat, p.gps_lng, p.fk_type, p.encrypted_name, ST_Contains(ST_GeomFromText(:polygon, 4326), POINT(CAST(p.gps_lng AS DECIMAL(10,8)), CAST(p.gps_lat AS DECIMAL(10,8)))) as is_inside FROM ope_pass p WHERE p.fk_sector = :sector_id AND p.gps_lat IS NOT NULL AND p.gps_lng IS NOT NULL"; $checkStmt = $this->db->prepare($checkPassagesQuery); $checkStmt->execute([ 'sector_id' => $sectorId, 'polygon' => $polygonString ]); $existingPassages = $checkStmt->fetchAll(); $passagesToDelete = []; $passagesToOrphan = []; foreach ($existingPassages as $passage) { if ($passage['is_inside'] == 0) { // Le passage est hors du nouveau périmètre if ($passage['fk_type'] == 2 && ($passage['encrypted_name'] === '' || $passage['encrypted_name'] === null)) { // Passage non visité : à supprimer $passagesToDelete[] = $passage['id']; $counters['passages_deleted']++; } else { // Passage visité : à mettre en orphelin $passagesToOrphan[] = $passage['id']; $counters['passages_orphaned']++; } } else { $counters['passages_kept']++; } } // Supprimer les passages non visités en une seule requête if (!empty($passagesToDelete)) { $placeholders = str_repeat('?,', count($passagesToDelete) - 1) . '?'; $deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)"; $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute($passagesToDelete); } // Mettre en orphelin les passages visités en une seule requête if (!empty($passagesToOrphan)) { $placeholders = str_repeat('?,', count($passagesToOrphan) - 1) . '?'; $orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id IN ($placeholders)"; $orphanStmt = $this->db->prepare($orphanQuery); $orphanStmt->execute($passagesToOrphan); } // 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE) // Récupérer toutes les adresses du secteur depuis sectors_adresses (avec colonnes bâtiments) $addressesQuery = "SELECT fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng, fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol FROM sectors_adresses WHERE fk_sector = :sector_id"; $addressesStmt = $this->db->prepare($addressesQuery); $addressesStmt->execute(['sector_id' => $sectorId]); $addresses = $addressesStmt->fetchAll(); 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)) { LogService::info('[updatePassagesForSector] Traitement des passages', [ 'user_id' => $firstUserId, 'nb_addresses' => count($addresses) ]); // Récupérer TOUS les passages existants pour cette opération en UNE requête $existingQuery = " SELECT id, fk_adresse, numero, rue, rue_bis, ville, residence, appt, fk_habitat, fk_type, encrypted_name, created_at FROM ope_pass WHERE fk_operation = :operation_id"; $existingStmt = $this->db->prepare($existingQuery); $existingStmt->execute(['operation_id' => $operationId]); $existingPassages = $existingStmt->fetchAll(); // Indexer les passages existants par clé : numero|rue|rue_bis|ville $passagesByAddress = []; foreach ($existingPassages as $p) { $addressKey = $p['numero'] . '|' . $p['rue'] . '|' . ($p['rue_bis'] ?? '') . '|' . $p['ville']; if (!isset($passagesByAddress[$addressKey])) { $passagesByAddress[$addressKey] = []; } $passagesByAddress[$addressKey][] = $p; } // Traiter chaque adresse du secteur $toInsert = []; $toUpdate = []; $toDelete = []; foreach ($addresses as $address) { $addressKey = $address['numero'] . '|' . $address['rue'] . '|' . ($address['rue_bis'] ?? '') . '|' . $address['ville']; $existingAtAddress = $passagesByAddress[$addressKey] ?? []; $nbExisting = count($existingAtAddress); $fkHabitat = $address['fk_habitat'] ?? 1; $nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1; $residence = $address['residence'] ?? ''; // IMPORTANT : Uniformisation GPS pour les immeubles // Tous les passages d'une même adresse doivent partager les mêmes coordonnées GPS // Issues de sectors_adresses (gps_lat, gps_lng) $gpsLat = $address['gps_lat']; $gpsLng = $address['gps_lng']; // CAS 1 : Maison individuelle (fk_habitat=1) if ($fkHabitat == 1) { if ($nbExisting == 0) { // INSERT 1 passage $toInsert[] = [ 'address' => $address, 'residence' => '', 'appt' => '', 'fk_habitat' => 1 ]; } else { // UPDATE le premier passage avec fk_habitat=1 $toUpdate[] = [ 'id' => $existingAtAddress[0]['id'], 'fk_habitat' => 1, 'residence' => '', 'gps_lat' => $gpsLat, 'gps_lng' => $gpsLng ]; // Les autres passages (si >1) ne sont PAS touchés } } // CAS 2 : Immeuble (fk_habitat=2) else if ($fkHabitat == 2) { // UPDATE TOUS les passages existants avec fk_habitat=2, residence et GPS foreach ($existingAtAddress as $existing) { $updates = [ 'id' => $existing['id'], 'fk_habitat' => 2, 'gps_lat' => $gpsLat, 'gps_lng' => $gpsLng ]; // Update residence seulement si non vide if (!empty($residence)) { $updates['residence'] = $residence; } $toUpdate[] = $updates; } // Si moins de nb_log passages : INSERT les manquants if ($nbExisting < $nbLog) { $nbToInsert = $nbLog - $nbExisting; for ($i = 0; $i < $nbToInsert; $i++) { $toInsert[] = [ 'address' => $address, 'residence' => $residence, 'appt' => '', // Pas de numéro d'appt prédéfini 'fk_habitat' => 2 ]; } } // Si plus de nb_log passages : DELETE les non visités en trop else if ($nbExisting > $nbLog) { $nbToDelete = $nbExisting - $nbLog; // Trier les passages par created_at ASC (les plus anciens d'abord) usort($existingAtAddress, function($a, $b) { return strtotime($a['created_at']) - strtotime($b['created_at']); }); $deleted = 0; foreach ($existingAtAddress as $existing) { if ($deleted >= $nbToDelete) break; // Supprimer seulement si fk_type=2 ET encrypted_name vide if ($existing['fk_type'] == 2 && ($existing['encrypted_name'] === '' || $existing['encrypted_name'] === null)) { $toDelete[] = $existing['id']; $deleted++; } } } } } // INSERT MULTIPLE en une seule requête if (!empty($toInsert)) { $values = []; $insertParams = []; $paramIndex = 0; foreach ($toInsert as $item) { $addr = $item['address']; $values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex, :num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex, :res$paramIndex, :appt$paramIndex, :habitat$paramIndex, :lat$paramIndex, :lng$paramIndex, 2, 0, '', NOW(), :creat$paramIndex, 1)"; $insertParams["op$paramIndex"] = $operationId; $insertParams["sect$paramIndex"] = $sectorId; $insertParams["usr$paramIndex"] = $firstUserId; $insertParams["addr$paramIndex"] = $addr['fk_adresse'] ?? null; $insertParams["num$paramIndex"] = $addr['numero']; $insertParams["rue$paramIndex"] = $addr['rue']; $insertParams["bis$paramIndex"] = $addr['rue_bis'] ?? ''; $insertParams["ville$paramIndex"] = $addr['ville']; $insertParams["res$paramIndex"] = $item['residence']; $insertParams["appt$paramIndex"] = $item['appt']; $insertParams["habitat$paramIndex"] = $item['fk_habitat']; $insertParams["lat$paramIndex"] = $addr['gps_lat']; $insertParams["lng$paramIndex"] = $addr['gps_lng']; $insertParams["creat$paramIndex"] = $_SESSION['user_id'] ?? null; $paramIndex++; } $insertQuery = "INSERT INTO ope_pass (fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville, residence, appt, fk_habitat, gps_lat, gps_lng, fk_type, nb_passages, encrypted_name, created_at, fk_user_creat, chk_active) VALUES " . implode(',', $values); try { $insertStmt = $this->db->prepare($insertQuery); $insertStmt->execute($insertParams); $counters['passages_created'] = count($toInsert); } catch (\Exception $e) { LogService::error('Erreur lors de l\'insertion multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); } } // UPDATE MULTIPLE avec CASE WHEN (inclut GPS pour uniformisation) if (!empty($toUpdate)) { $updateIds = array_column($toUpdate, 'id'); $placeholders = str_repeat('?,', count($updateIds) - 1) . '?'; $caseWhenHabitat = []; $caseWhenResidence = []; $caseWhenGpsLat = []; $caseWhenGpsLng = []; $updateParams = []; foreach ($toUpdate as $upd) { // fk_habitat est toujours présent $caseWhenHabitat[] = "WHEN id = ? THEN ?"; $updateParams[] = $upd['id']; $updateParams[] = $upd['fk_habitat']; // GPS : toujours présent maintenant (uniformisation) if (isset($upd['gps_lat']) && isset($upd['gps_lng'])) { $caseWhenGpsLat[] = "WHEN id = ? THEN ?"; $updateParams[] = $upd['id']; $updateParams[] = $upd['gps_lat']; $caseWhenGpsLng[] = "WHEN id = ? THEN ?"; $updateParams[] = $upd['id']; $updateParams[] = $upd['gps_lng']; } // residence est optionnel if (isset($upd['residence'])) { $caseWhenResidence[] = "WHEN id = ? THEN ?"; $updateParams[] = $upd['id']; $updateParams[] = $upd['residence']; } } $setClause = ["fk_habitat = CASE " . implode(' ', $caseWhenHabitat) . " ELSE fk_habitat END"]; if (!empty($caseWhenGpsLat)) { $setClause[] = "gps_lat = CASE " . implode(' ', $caseWhenGpsLat) . " ELSE gps_lat END"; } if (!empty($caseWhenGpsLng)) { $setClause[] = "gps_lng = CASE " . implode(' ', $caseWhenGpsLng) . " ELSE gps_lng END"; } if (!empty($caseWhenResidence)) { $setClause[] = "residence = CASE " . implode(' ', $caseWhenResidence) . " ELSE residence END"; } $updateQuery = "UPDATE ope_pass SET " . implode(', ', $setClause) . " WHERE id IN ($placeholders)"; try { $updateStmt = $this->db->prepare($updateQuery); $updateStmt->execute(array_merge($updateParams, $updateIds)); $counters['passages_updated'] = count($toUpdate); // Log pour vérifier l'uniformisation GPS (surtout pour immeubles) LogService::info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [ 'nb_updated' => count($toUpdate), 'sector_id' => $sectorId ]); } catch (\Exception $e) { LogService::error('Erreur lors de la mise à jour multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); } } // DELETE MULTIPLE en une seule requête if (!empty($toDelete)) { $placeholders = str_repeat('?,', count($toDelete) - 1) . '?'; $deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)"; try { $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute($toDelete); $counters['passages_deleted'] += count($toDelete); } catch (\Exception $e) { LogService::error('Erreur lors de la suppression multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); } } } else { 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 LogService::info('[updatePassagesForSector] Fin traitement', [ 'sector_id' => $sectorId, 'counters' => $counters ]); return $counters; } catch (\Exception $e) { 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 ]; } } }