feat: Gestion des secteurs et migration v3.0.4+304

- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 3bbc599ab4
commit 1018b86537
620 changed files with 120502 additions and 91396 deletions

27
api/src/Config/AppConfig.php Normal file → Executable file
View File

@@ -88,6 +88,12 @@ class AppConfig {
'username' => 'geo_app_user_prod',
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
],
'addresses_database' => [
'host' => '13.23.33.26',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeo.User',
],
]);
// Configuration RECETTE
@@ -99,6 +105,12 @@ class AppConfig {
'username' => 'geo_app_user_rec',
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
],
'addresses_database' => [
'host' => '13.23.33.36',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoRec.User',
],
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
]);
@@ -111,6 +123,12 @@ class AppConfig {
'username' => 'geo_app_user_dev',
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
],
'addresses_database' => [
'host' => '13.23.33.46',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoDev.User',
],
// Vous pouvez activer des fonctionnalités de débogage en développement
'debug' => true,
// Configurez des endpoints de test pour Stripe, etc.
@@ -234,6 +252,15 @@ class AppConfig {
return $this->getCurrentConfig()['database'];
}
/**
* Retourne la configuration de la base de données des adresses
*
* @return array Configuration de la base de données des adresses
*/
public function getAddressesDatabaseConfig(): array {
return $this->getCurrentConfig()['addresses_database'];
}
/**
* Retourne la clé de chiffrement
*

0
api/src/Controllers/EntiteController.php Normal file → Executable file
View File

0
api/src/Controllers/FileController.php Normal file → Executable file
View File

0
api/src/Controllers/LogController.php Normal file → Executable file
View File

30
api/src/Controllers/LoginController.php Normal file → Executable file
View File

@@ -49,8 +49,8 @@ class LoginController {
// Récupérer le type d'utilisateur
// admin accessible uniquement aux fk_role>1
// sinon tout user peut se connecter à l'interface utilisateur
$roleCondition = ($interface === 'user') ? '' : 'AND fk_role>1';
// user accessible uniquement aux fk_role=1
$roleCondition = ($interface === 'user') ? 'AND fk_role=1' : 'AND fk_role>1';
// Log pour le debug
LogService::log('Tentative de connexion GeoSector', [
@@ -159,6 +159,32 @@ class LoginController {
'fk_entite' => $user['fk_entite'] ?? '0',
];
Session::login($sessionData);
// Vérifier et exécuter l'initialisation des contours départementaux pour d6soft
if ($username === 'd6soft') {
require_once __DIR__ . '/../../scripts/init_departements_contours.php';
$initLog = \DepartementContoursInitializer::runIfNeeded($this->db, $username);
if ($initLog !== null) {
// Logger l'initialisation
LogService::log('Initialisation des contours départementaux', [
'level' => 'info',
'username' => $username,
'log_count' => count($initLog)
]);
// Logger aussi les dernières lignes du log pour diagnostic
$lastLines = array_slice($initLog, -5);
foreach ($lastLines as $line) {
if (strpos($line, '✗') !== false || strpos($line, 'terminé') !== false) {
LogService::log('Import contours: ' . $line, [
'level' => 'info',
'username' => $username
]);
}
}
}
}
// Préparation des données utilisateur pour la réponse (uniquement les champs du user)
$userData = [

0
api/src/Controllers/OperationController.php Normal file → Executable file
View File

0
api/src/Controllers/PassageController.php Normal file → Executable file
View File

File diff suppressed because it is too large Load Diff

162
api/src/Controllers/UserController.php Normal file → Executable file
View File

@@ -550,28 +550,27 @@ class UserController {
// ——— Gestion du transfert éventuel ———
$transferTo = isset($_GET['transfer_to']) ? trim($_GET['transfer_to']) : null;
$operationId = isset($_GET['operation_id']) ? trim($_GET['operation_id']) : null;
if (($transferTo && !$operationId) || (!$transferTo && $operationId)) {
Response::json([
'status' => 'error',
'message' => "Il faut fournir transfer_to ET operation_id ou aucun des deux"
], 400);
return;
}
if ($transferTo && $operationId) {
if ($transferTo) {
try {
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
$stmt3 = $this->db->prepare('
UPDATE passages
UPDATE ope_pass
SET fk_user = :new_user_id
WHERE fk_user = :delete_user_id
AND fk_operation = :operation_id
');
$stmt3->execute([
'new_user_id' => $transferTo,
'delete_user_id' => $id,
'operation_id' => $operationId
'delete_user_id' => $id
]);
$transferredCount = $stmt3->rowCount();
LogService::log('Passages transférés avant suppression utilisateur', [
'level' => 'info',
'from_user' => $id,
'to_user' => $transferTo,
'passages_transferred' => $transferredCount
]);
} catch (PDOException $e) {
Response::json([
@@ -589,7 +588,9 @@ class UserController {
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
$stmtOpeUsers->execute([$id]);
// Ici éventuellement : d'autres suppressions en cascade si besoin
// Supprimer les enregistrements dépendants dans ope_users_sectors
$stmtOpeUsersSectors = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_user = ?');
$stmtOpeUsersSectors->execute([$id]);
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
@@ -606,7 +607,7 @@ class UserController {
'level' => 'info',
'deletedBy' => $currentUserId,
'userId' => $id,
'passage_transfer' => $transferTo && $operationId ? "Vers utilisateur $transferTo pour operation $operationId" : 'Aucun'
'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
]);
Response::json([
@@ -626,6 +627,135 @@ class UserController {
}
}
public function resetPassword(string $id): void {
Session::requireAuth();
$currentUserId = Session::getUserId();
// Récupérer les infos de l'utilisateur courant
$stmt = $this->db->prepare('SELECT fk_role, fk_entite, chk_active FROM users WHERE id = ?');
$stmt->execute([$currentUserId]);
$currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$currentUser) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur courant non trouvé'
], 403);
return;
}
// Vérifier que l'utilisateur courant est actif
if ($currentUser['chk_active'] != 1) {
Response::json([
'status' => 'error',
'message' => 'Votre compte n\'est pas actif'
], 403);
return;
}
$userRole = (int)$currentUser['fk_role'];
$userEntite = $currentUser['fk_entite'];
// Récupérer l'utilisateur cible
$stmt = $this->db->prepare('
SELECT id, encrypted_email, encrypted_name, fk_entite, chk_active
FROM users
WHERE id = ?
');
$stmt->execute([$id]);
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$targetUser) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur non trouvé'
], 404);
return;
}
// Vérifier que l'utilisateur est actif
if ($targetUser['chk_active'] != 1) {
Response::json([
'status' => 'error',
'message' => 'L\'utilisateur n\'est pas actif'
], 400);
return;
}
// Contrôle des droits selon le rôle
if ($userRole === 1) {
// Role 1 : peut uniquement réinitialiser son propre mot de passe
if ($currentUserId != $id) {
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez réinitialiser que votre propre mot de passe'
], 403);
return;
}
} elseif ($userRole === 2) {
// Role 2 : peut réinitialiser les mots de passe de sa propre entité
if ($userEntite != $targetUser['fk_entite']) {
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez réinitialiser que les mots de passe des utilisateurs de votre entité'
], 403);
return;
}
}
// Role > 2 : peut tout faire
try {
// Déchiffrement des données
$email = ApiService::decryptSearchableData($targetUser['encrypted_email']);
$name = ApiService::decryptData($targetUser['encrypted_name']);
// Génération d'un nouveau mot de passe sécurisé
$newPassword = ApiService::generateSecurePassword();
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
// Mise à jour du mot de passe en base de données
$updateStmt = $this->db->prepare('
UPDATE users
SET user_pass_hash = :password,
updated_at = NOW(),
fk_user_modif = :modifier_id
WHERE id = :id
');
$updateStmt->execute([
'password' => $passwordHash,
'modifier_id' => $currentUserId,
'id' => $id
]);
// Envoi de l'email avec le nouveau mot de passe
ApiService::sendEmail($email, $name, 'password_reset', ['password' => $newPassword]);
LogService::log('Mot de passe réinitialisé', [
'level' => 'info',
'resetBy' => $currentUserId,
'userId' => $id,
'email' => $email
]);
Response::json([
'status' => 'success',
'message' => 'Mot de passe réinitialisé avec succès. Un email a été envoyé à l\'utilisateur.'
]);
} catch (PDOException $e) {
LogService::log('Erreur lors de la réinitialisation du mot de passe', [
'level' => 'error',
'error' => $e->getMessage(),
'userId' => $id
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
// Méthodes auxiliaires
private function validateUpdateData(array $data): ?string {
// Validation de l'email

0
api/src/Controllers/VilleController.php Normal file → Executable file
View File

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
class AddressesDatabase {
private static ?PDO $instance = null;
private static array $config;
public static function init(array $config): void {
self::$config = $config;
}
public static function getInstance(): PDO {
if (self::$instance === null) {
try {
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",
self::$config['host'],
self::$config['name']
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
self::$instance = new PDO(
$dsn,
self::$config['username'],
self::$config['password'],
$options
);
} catch (PDOException $e) {
throw new RuntimeException("Addresses database connection failed: " . $e->getMessage());
}
}
return self::$instance;
}
/**
* Ferme la connexion à la base de données des adresses
*/
public static function close(): void {
self::$instance = null;
}
}

0
api/src/Core/Database.php Normal file → Executable file
View File

0
api/src/Core/Request.php Normal file → Executable file
View File

0
api/src/Core/Response.php Normal file → Executable file
View File

7
api/src/Core/Router.php Normal file → Executable file
View File

@@ -37,6 +37,7 @@ class Router {
$this->post('users', ['UserController', 'createUser']);
$this->put('users/:id', ['UserController', 'updateUser']);
$this->delete('users/:id', ['UserController', 'deleteUser']);
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
$this->post('logout', ['LoginController', 'logout']);
// Routes entités
@@ -80,6 +81,12 @@ class Router {
$this->get('files/info/:id', ['FileController', 'getFileInfo']);
$this->get('files/download/:id', ['FileController', 'download']);
$this->delete('files/:id', ['FileController', 'deleteFile']);
// Routes secteurs
$this->get('sectors', ['SectorController', 'index']);
$this->post('sectors', ['SectorController', 'create']);
$this->put('sectors/:id', ['SectorController', 'update']);
$this->delete('sectors/:id', ['SectorController', 'delete']);
}
public function handle(): void {

5
api/src/Core/Session.php Normal file → Executable file
View File

@@ -27,6 +27,7 @@ class Session {
public static function login(array $userData): void {
$_SESSION['user_id'] = $userData['id'];
$_SESSION['user_email'] = $userData['email'] ?? '';
$_SESSION['entity_id'] = $userData['fk_entite'] ?? null;
$_SESSION['authenticated'] = true;
$_SESSION['last_activity'] = time();
@@ -51,6 +52,10 @@ class Session {
return $_SESSION['user_email'] ?? null;
}
public static function getEntityId(): ?int {
return $_SESSION['entity_id'] ?? null;
}
public static function requireAuth(): void {
if (!self::isAuthenticated()) {
// Log détaillé pour le debug

View File

@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/LogService.php';
class AddressService {
private ?PDO $addressesDb = null;
private PDO $mainDb;
private LogService $logService;
public function __construct() {
$this->logService = new LogService();
try {
$this->addressesDb = AddressesDatabase::getInstance();
$this->logService->info('[AddressService] Connexion à la base d\'adresses réussie');
} catch (\Exception $e) {
// Si la connexion échoue, on continue sans la base d'adresses
$this->logService->error('[AddressService] Connexion à la base d\'adresses impossible', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$this->addressesDb = null;
}
$this->mainDb = Database::getInstance();
}
/**
* Vérifie si la connexion à la base d'adresses est active
* @return bool
*/
public function isConnected(): bool {
return $this->addressesDb !== null;
}
/**
* Détermine le département de l'entité courante
*
* @param int|null $entityId ID de l'entité
* @return string|null Code département (ex: "22", "23")
*/
private function getDepartmentForEntity(?int $entityId = null): ?string {
if (!$entityId) {
$entityId = $_SESSION['entity_id'] ?? null;
}
if (!$entityId) {
return null;
}
try {
$query = "SELECT departement FROM entites WHERE id = :entity_id";
$stmt = $this->mainDb->prepare($query);
$stmt->execute(['entity_id' => $entityId]);
$result = $stmt->fetch();
return $result ? $result['departement'] : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return array Array des adresses trouvées
*/
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array {
// Si pas de connexion à la base d'adresses, retourner un tableau vide
if (!$this->addressesDb) {
$this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [
'entity_id' => $entityId
]);
return [];
}
$this->logService->info('[AddressService] Début recherche adresses', [
'entity_id' => $entityId,
'nb_coordinates' => count($coordinates)
]);
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
require_once __DIR__ . '/DepartmentBoundaryService.php';
$boundaryService = new \DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé par analyse spatiale,
// chercher d'abord dans le département de l'entité et ses limitrophes
$entityDept = $this->getDepartmentForEntity($entityId);
$this->logService->info('[AddressService] Département de l\'entité', [
'departement' => $entityDept
]);
if (!$entityDept) {
$this->logService->error('[AddressService] Impossible de déterminer le département de l\'entité', [
'entity_id' => $entityId
]);
throw new RuntimeException("Impossible de déterminer le département");
}
// Obtenir les départements prioritaires (entité + limitrophes)
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
// Log pour debug
$this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [
'departements_prioritaires' => implode(', ', $priorityDepts)
]);
// Utiliser les départements prioritaires pour la recherche
$departmentsTouched = [];
foreach ($priorityDepts as $deptCode) {
$departmentsTouched[] = ['code_dept' => $deptCode];
}
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Collecter les adresses de tous les départements touchés
$allAddresses = [];
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
// Requête pour récupérer les adresses dans le polygone pour ce département
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
x,
y,
code_insee,
nom_ld,
ville_acheminement,
rue_afnor,
source,
certification,
:dept_code as departement
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'polygon' => $polygonString,
'dept_code' => $deptCode
]);
$addresses = $stmt->fetchAll();
// Ajouter les adresses à la collection globale
foreach ($addresses as $address) {
$allAddresses[] = $address;
}
// Log pour debug
$this->logService->info('[AddressService] Recherche dans table', [
'table' => $tableName,
'departement' => $deptCode,
'nb_adresses' => count($addresses)
]);
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
$this->logService->error('[AddressService] Erreur SQL', [
'table' => $tableName,
'departement' => $deptCode,
'error' => $e->getMessage(),
'code' => $e->getCode()
]);
}
}
$this->logService->info('[AddressService] Fin recherche adresses', [
'total_adresses' => count($allAddresses)
]);
return $allAddresses;
}
/**
* Récupère les adresses dans un rayon autour d'un point
*
* @param float $latitude Latitude du centre
* @param float $longitude Longitude du centre
* @param float $radiusMeters Rayon en mètres
* @param int|null $entityId ID de l'entité (pour déterminer le département)
* @return array Array des adresses trouvées
*/
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array {
// Déterminer le département
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département de l'entité");
}
// Nom de la table selon le département
$tableName = "cp" . $dept;
try {
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) as distance
FROM `$tableName`
WHERE ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) <= :radius
AND gps_lat != ''
AND gps_lng != ''
ORDER BY distance";
$point = "POINT($longitude $latitude)";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'point' => $point,
'radius' => $radiusMeters
]);
return $stmt->fetchAll();
} catch (PDOException $e) {
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
}
}
/**
* Compte le nombre d'adresses dans un polygone
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return int Nombre d'adresses
*/
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int {
// Si pas de connexion à la base d'adresses, retourner 0
if (!$this->addressesDb) {
error_log("AddressService: Pas de connexion à la base d'adresses, retour de 0 adresses");
return 0;
}
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
require_once __DIR__ . '/DepartmentBoundaryService.php';
$boundaryService = new \DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé, utiliser le département de l'entité
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département");
}
$departmentsTouched = [['code_dept' => $dept]];
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Compter les adresses dans tous les départements touchés
$totalCount = 0;
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
$sql = "SELECT COUNT(*) as count
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute(['polygon' => $polygonString]);
$result = $stmt->fetch();
$deptCount = (int)$result['count'];
$totalCount += $deptCount;
// Log pour debug
error_log("Département $deptCode : $deptCount adresses comptées");
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
}
}
return $totalCount;
}
}

0
api/src/Services/ApiService.php Normal file → Executable file
View File

0
api/src/Services/BackupEncryptionService.php Normal file → Executable file
View File

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
class DepartmentBoundaryService {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Vérifie si un polygone (secteur) est entièrement contenu dans un département
*
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
* @param string $departmentCode Code du département (22, 29, etc.)
* @return array ['is_contained' => bool, 'message' => string, 'intersecting_departments' => array]
*/
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array {
if (count($sectorCoordinates) < 3) {
return [
'is_contained' => false,
'message' => 'Un secteur doit avoir au moins 3 points',
'intersecting_departments' => []
];
}
// Créer le polygone du secteur
$polygonPoints = [];
foreach ($sectorCoordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
return [
'is_contained' => false,
'message' => 'Coordonnées invalides',
'intersecting_departments' => []
];
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
try {
// 1. Vérifier si le secteur est entièrement dans le département cible
$sql = "SELECT
code_dept,
nom_dept,
ST_Contains(contour, ST_GeomFromText(:sector_polygon, 4326)) as is_contained,
ST_Intersects(contour, ST_GeomFromText(:sector_polygon, 4326)) as intersects
FROM x_departements
WHERE code = :dept_code
AND contour IS NOT NULL";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon' => $sectorPolygon,
'dept_code' => $departmentCode
]);
$targetDept = $stmt->fetch();
if (!$targetDept) {
return [
'is_contained' => false,
'message' => "Le département $departmentCode n'a pas de contour défini",
'intersecting_departments' => []
];
}
// 2. Si le secteur n'est pas entièrement contenu, trouver tous les départements qu'il touche
if (!$targetDept['is_contained']) {
$sql = "SELECT
code_dept,
nom_dept,
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
FROM x_departements_contours
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
ORDER BY percentage_overlap DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon1' => $sectorPolygon,
'sector_polygon2' => $sectorPolygon,
'sector_polygon3' => $sectorPolygon
]);
$intersectingDepts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Formater le message
$deptsList = array_map(function($d) {
return sprintf("%s (%s) : %.1f%%",
$d['nom_dept'],
$d['code_dept'],
$d['percentage_overlap']
);
}, $intersectingDepts);
return [
'is_contained' => false,
'message' => "Le secteur déborde du département {$targetDept['nom_dept']}. Il est à cheval sur : " . implode(', ', $deptsList),
'intersecting_departments' => $intersectingDepts
];
}
return [
'is_contained' => true,
'message' => "Le secteur est entièrement contenu dans le département {$targetDept['nom_dept']}",
'intersecting_departments' => [$targetDept]
];
} catch (\PDOException $e) {
throw new RuntimeException("Erreur lors de la vérification des limites départementales : " . $e->getMessage());
}
}
/**
* Récupère les départements qui intersectent avec un secteur
*
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
* @return array Liste des départements avec leur pourcentage de recouvrement
*/
public function getDepartmentsForSector(array $sectorCoordinates): array {
if (count($sectorCoordinates) < 3) {
return [];
}
// Créer le polygone du secteur
$polygonPoints = [];
foreach ($sectorCoordinates as $coord) {
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
}
$polygonPoints[] = $polygonPoints[0];
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
try {
$sql = "SELECT
code_dept,
nom_dept,
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
FROM x_departements_contours
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
ORDER BY percentage_overlap DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon1' => $sectorPolygon,
'sector_polygon2' => $sectorPolygon,
'sector_polygon3' => $sectorPolygon
]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
throw new RuntimeException("Erreur lors de la recherche des départements : " . $e->getMessage());
}
}
/**
* Vérifie si les contours des départements sont chargés
*
* @return array ['loaded' => bool, 'count' => int, 'missing' => array]
*/
public function checkDepartmentContoursStatus(): array {
try {
// Compter les départements avec contours
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
$stmt = $this->db->query($sql);
$count = $stmt->fetch()['count'];
// Récupérer la liste des départements utilisés dans les entités
$sql = "SELECT DISTINCT departement
FROM entites
WHERE departement IS NOT NULL
ORDER BY departement";
$stmt = $this->db->query($sql);
$usedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
// Vérifier lesquels ont des contours
$sql = "SELECT code_dept FROM x_departements_contours WHERE code_dept IN ('" . implode("','", $usedDepts) . "')";
$stmt = $this->db->query($sql);
$loadedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
$missingDepts = array_diff($usedDepts, $loadedDepts);
return [
'loaded' => count($missingDepts) === 0,
'count' => $count,
'total_used' => count($usedDepts),
'missing' => array_values($missingDepts)
];
} catch (\PDOException $e) {
return [
'loaded' => false,
'count' => 0,
'total_used' => 0,
'missing' => []
];
}
}
/**
* Récupère les départements limitrophes d'un département donné
*
* @param string $departmentCode Code du département
* @return array Array des codes départements limitrophes
*/
public function getAdjacentDepartments(string $departmentCode): array {
try {
$sql = "SELECT dept_limitrophes FROM x_departements WHERE code = :dept_code AND chk_active = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute(['dept_code' => $departmentCode]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result || empty($result['dept_limitrophes'])) {
return [];
}
// Convertir la chaîne CSV en array
return array_map('trim', explode(',', $result['dept_limitrophes']));
} catch (PDOException $e) {
error_log("Erreur lors de la récupération des départements limitrophes : " . $e->getMessage());
return [];
}
}
/**
* Récupère les départements à prioriser pour la recherche
* (département de l'entité + ses limitrophes)
*
* @param string $entityDepartment Code du département de l'entité
* @return array Array des codes départements à prioriser
*/
public function getPriorityDepartments(string $entityDepartment): array {
$priorityDepts = [$entityDepartment]; // Commencer par le département de l'entité
// Ajouter les départements limitrophes
$adjacentDepts = $this->getAdjacentDepartments($entityDepartment);
$priorityDepts = array_merge($priorityDepts, $adjacentDepts);
// Retourner sans doublons
return array_unique($priorityDepts);
}
}

0
api/src/Services/EmailTemplates.php Normal file → Executable file
View File

0
api/src/Services/ExportService.php Normal file → Executable file
View File

0
api/src/Services/FileService.php Normal file → Executable file
View File

48
api/src/Services/LogService.php Normal file → Executable file
View File

@@ -25,6 +25,19 @@ class LogService {
if ($clientType === 'mobile' && isset($clientInfo['appIdentifier'])) {
$defaultMetadata['app_identifier'] = $clientInfo['appIdentifier'];
}
// Ajouter les informations de session si disponibles
if (session_status() === PHP_SESSION_ACTIVE) {
if (isset($_SESSION['user_id'])) {
$defaultMetadata['user_id'] = $_SESSION['user_id'];
}
if (isset($_SESSION['entity_id'])) {
$defaultMetadata['entity_id'] = $_SESSION['entity_id'];
}
if (isset($_SESSION['operation_id'])) {
$defaultMetadata['operation_id'] = $_SESSION['operation_id'];
}
}
$metadata = array_merge_recursive($defaultMetadata, $metadata);
@@ -73,15 +86,31 @@ class LogService {
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
$timestamp = date('Y-m-d\TH:i:s');
$browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version'];
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
// Ne pas afficher l'OS s'il est unknown
$osInfo = '';
if ($clientInfo['os']['name'] !== 'unknown' && $clientInfo['os']['version'] !== 'unknown') {
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
}
// Extraire le niveau de log
$level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info';
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format)
$additionalMetadata = [];
// Ajouter user_id, entity_id et operation_id en premier s'ils existent
$priorityKeys = ['user_id', 'entity_id', 'operation_id'];
foreach ($priorityKeys as $key) {
if (isset($metadata[$key]) && !is_array($metadata[$key])) {
$additionalMetadata[$key] = $metadata[$key];
}
}
// Ajouter les autres métadonnées
foreach ($metadata as $key => $value) {
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])) {
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])
&& !in_array($key, $priorityKeys)) {
if (is_array($value)) {
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} else {
@@ -114,4 +143,19 @@ class LogService {
error_log("Erreur lors de l'écriture des logs: " . $e->getMessage());
}
}
public function info(string $message, array $metadata = []): void {
$metadata['level'] = 'info';
self::log($message, $metadata);
}
public function warning(string $message, array $metadata = []): void {
$metadata['level'] = 'warning';
self::log($message, $metadata);
}
public function error(string $message, array $metadata = []): void {
$metadata['level'] = 'error';
self::log($message, $metadata);
}
}

0
api/src/Services/OperationDataService.php Normal file → Executable file
View File

0
api/src/Utils/ClientDetector.php Normal file → Executable file
View File