Files
geo/api/src/Controllers/SectorController.php
Pierre 232940b1eb feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques
- #18: Optimisation services API et logs
- #19: Corrections Flutter widgets et repositories
- #20: Fix création passage - détection automatique ope_users.id vs users.id

Suppression dossier web/ (migration vers app Flutter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:11:15 +01:00

1718 lines
80 KiB
PHP

<?php
namespace App\Controllers;
use Database;
use Response;
use App\Services\LogService;
use App\Services\EventLogService;
use App\Services\ApiService;
use App\Services\AddressService;
use App\Services\DepartmentBoundaryService;
require_once __DIR__ . '/../Services/EventLogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
class SectorController
{
private \PDO $db;
private AddressService $addressService;
private DepartmentBoundaryService $boundaryService;
public function __construct()
{
$this->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
];
}
}
}