feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,13 +7,19 @@ namespace App\Controllers;
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
// Les classes sont déjà incluses via require_once, pas besoin de 'use' statements
|
||||
use PDO;
|
||||
use Database;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
class ChatController {
|
||||
private \PDO $db;
|
||||
private PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Database::getInstance();
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +30,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
// Vérifier si c'est une synchronisation incrémentale
|
||||
$updatedAfter = $_GET['updated_after'] ?? null;
|
||||
@@ -186,7 +192,7 @@ class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'sync_timestamp' => $syncTimestamp,
|
||||
'has_changes' => !empty($rooms),
|
||||
@@ -194,11 +200,11 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des conversations', [
|
||||
LogService::log('Erreur lors de la récupération des conversations', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -213,9 +219,9 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
@@ -223,7 +229,7 @@ class ChatController {
|
||||
|
||||
// Validation des données
|
||||
if (!isset($data['type']) || !in_array($data['type'], ['private', 'group', 'broadcast'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Type de conversation invalide'
|
||||
], 400);
|
||||
@@ -233,7 +239,7 @@ class ChatController {
|
||||
// Vérification des permissions pour broadcast
|
||||
// Seuls les super admins (role = 9) peuvent créer des broadcasts
|
||||
if ($data['type'] === 'broadcast' && $userRole != 9) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seuls les super administrateurs peuvent créer des annonces'
|
||||
], 403);
|
||||
@@ -242,7 +248,7 @@ class ChatController {
|
||||
|
||||
// Validation des participants
|
||||
if (!isset($data['participants']) || !is_array($data['participants']) || empty($data['participants'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Au moins un participant requis'
|
||||
], 400);
|
||||
@@ -251,7 +257,7 @@ class ChatController {
|
||||
|
||||
// Pour une conversation privée, limiter à 2 participants (incluant le créateur)
|
||||
if ($data['type'] === 'private' && count($data['participants']) > 1) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une conversation privée ne peut avoir que 2 participants'
|
||||
], 400);
|
||||
@@ -272,7 +278,7 @@ class ChatController {
|
||||
if ($tempId !== null) {
|
||||
$existingRoom['temp_id'] = $tempId;
|
||||
}
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $existingRoom,
|
||||
'existing' => true
|
||||
@@ -351,7 +357,7 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation créée', [
|
||||
LogService::log('Conversation créée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'type' => $data['type'],
|
||||
@@ -367,7 +373,7 @@ class ChatController {
|
||||
$room['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $room
|
||||
], 201);
|
||||
@@ -378,20 +384,20 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
} catch (\Exception $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 400);
|
||||
@@ -406,8 +412,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -423,7 +429,7 @@ class ChatController {
|
||||
$room = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée ou accès non autorisé'
|
||||
], 404);
|
||||
@@ -432,7 +438,7 @@ class ChatController {
|
||||
|
||||
// Vérifier les permissions
|
||||
if ($room['created_by'] != $userId && !$room['is_admin']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur ou un admin peut modifier la conversation'
|
||||
], 403);
|
||||
@@ -460,24 +466,24 @@ class ChatController {
|
||||
$updatedRoom['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Conversation mise à jour', [
|
||||
LogService::log('Conversation mise à jour', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $updatedRoom
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -493,7 +499,7 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que la room existe et récupérer le créateur
|
||||
$stmt = $this->db->prepare('
|
||||
@@ -506,7 +512,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room existe
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée'
|
||||
], 404);
|
||||
@@ -515,7 +521,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room n'est pas déjà supprimée
|
||||
if ($room['is_active'] == 0) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cette conversation est déjà supprimée'
|
||||
], 400);
|
||||
@@ -524,7 +530,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if ($room['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur de la conversation peut la supprimer'
|
||||
], 403);
|
||||
@@ -554,13 +560,13 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation supprimée', [
|
||||
LogService::log('Conversation supprimée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'deleted_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Conversation supprimée avec succès'
|
||||
]);
|
||||
@@ -573,12 +579,12 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -593,11 +599,11 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -658,7 +664,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = ($message['sender_id'] == $userId);
|
||||
}
|
||||
@@ -675,7 +681,7 @@ class ChatController {
|
||||
// Compter les messages non lus restants (devrait être 0)
|
||||
$unreadCount = $this->getUnreadCount($roomId, $userId);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'messages' => $messages,
|
||||
'has_more' => count($messages) === $limit,
|
||||
@@ -684,12 +690,12 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des messages', [
|
||||
LogService::log('Erreur lors de la récupération des messages', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -704,15 +710,15 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -724,7 +730,7 @@ class ChatController {
|
||||
if ($roomInfo && $roomInfo['type'] === 'broadcast') {
|
||||
// Pour les broadcasts, seul le créateur peut écrire
|
||||
if ($roomInfo['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul l\'administrateur peut poster dans une annonce'
|
||||
], 403);
|
||||
@@ -733,7 +739,7 @@ class ChatController {
|
||||
} else {
|
||||
// Pour les autres types, vérifier can_write
|
||||
if (!$this->canUserWrite($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous n\'avez pas la permission d\'écrire dans cette conversation'
|
||||
], 403);
|
||||
@@ -743,7 +749,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -754,7 +760,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -799,7 +805,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$message = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_mine'] = true;
|
||||
$message['is_read'] = false;
|
||||
$message['read_count'] = 0;
|
||||
@@ -809,25 +815,25 @@ class ChatController {
|
||||
$message['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message envoyé', [
|
||||
LogService::log('Message envoyé', [
|
||||
'level' => 'debug',
|
||||
'room_id' => $roomId,
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $message
|
||||
], 201);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de l\'envoi du message', [
|
||||
LogService::log('Erreur lors de l\'envoi du message', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -842,8 +848,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -858,7 +864,7 @@ class ChatController {
|
||||
$message = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$message) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message non trouvé'
|
||||
], 404);
|
||||
@@ -867,7 +873,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le sender du message
|
||||
if ($message['sender_id'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous ne pouvez modifier que vos propres messages'
|
||||
], 403);
|
||||
@@ -876,7 +882,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que le message n'est pas supprimé
|
||||
if ($message['is_deleted']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Ce message a été supprimé'
|
||||
], 400);
|
||||
@@ -885,7 +891,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -896,7 +902,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -931,7 +937,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$updatedMessage = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$updatedMessage['sender_name'] = \ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['sender_name'] = ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['is_mine'] = true;
|
||||
|
||||
// Ajouter le temp_id à la réponse si fourni
|
||||
@@ -939,24 +945,24 @@ class ChatController {
|
||||
$updatedMessage['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message modifié', [
|
||||
LogService::log('Message modifié', [
|
||||
'level' => 'debug',
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $updatedMessage
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la modification du message', [
|
||||
LogService::log('Erreur lors de la modification du message', [
|
||||
'level' => 'error',
|
||||
'message_id' => $messageId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -971,12 +977,12 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -1041,18 +1047,18 @@ class ChatController {
|
||||
]);
|
||||
$result = $countStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'unread_count' => (int)$result['unread_count']
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors du marquage comme lu', [
|
||||
LogService::log('Erreur lors du marquage comme lu', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1067,8 +1073,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
$sql = '
|
||||
@@ -1122,11 +1128,11 @@ class ChatController {
|
||||
|
||||
foreach ($recipients as &$recipient) {
|
||||
// Déchiffrer le nom
|
||||
$recipient['name'] = \ApiService::decryptData($recipient['name']);
|
||||
$recipient['name'] = ApiService::decryptData($recipient['name']);
|
||||
|
||||
// Déchiffrer le nom de l'entité
|
||||
$entiteName = $recipient['entite_name'] ?
|
||||
\ApiService::decryptData($recipient['entite_name']) :
|
||||
ApiService::decryptData($recipient['entite_name']) :
|
||||
'Sans entité';
|
||||
|
||||
// Créer une copie pour recipients_by_entity
|
||||
@@ -1146,18 +1152,18 @@ class ChatController {
|
||||
$recipientsDecrypted[] = $recipient;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'recipients' => $recipientsDecrypted,
|
||||
'recipients_by_entity' => $recipientsByEntity
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1225,7 +1231,7 @@ class ChatController {
|
||||
$participants = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($participants as &$participant) {
|
||||
$participant['name'] = \ApiService::decryptData($participant['name']);
|
||||
$participant['name'] = ApiService::decryptData($participant['name']);
|
||||
}
|
||||
|
||||
return $participants;
|
||||
@@ -1349,7 +1355,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
@@ -1398,7 +1404,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
@@ -14,8 +15,10 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\FileService;
|
||||
use Exception;
|
||||
|
||||
class EntiteController {
|
||||
@@ -74,13 +77,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
(int)$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $entiteId,
|
||||
@@ -220,12 +222,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector via getOrCreateEntiteByPostalCode', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return $entiteId;
|
||||
} catch (Exception $e) {
|
||||
@@ -559,10 +561,8 @@ class EntiteController {
|
||||
$params[] = $data['gps_lng'];
|
||||
}
|
||||
|
||||
if (isset($data['stripe_id'])) {
|
||||
$updateFields[] = 'encrypted_stripe_id = ?';
|
||||
$params[] = ApiService::encryptData($data['stripe_id']);
|
||||
}
|
||||
// Note: stripe_id ne peut plus être modifié ici
|
||||
// Les données Stripe sont gérées via la table stripe_accounts
|
||||
|
||||
if (isset($data['chk_demo'])) {
|
||||
$updateFields[] = 'chk_demo = ?';
|
||||
@@ -629,12 +629,23 @@ class EntiteController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Mise à jour d\'une entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
// Log de mise à jour de l'entité
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logEntityUpdated((int)$entiteId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -738,7 +749,7 @@ class EntiteController {
|
||||
|
||||
// Créer le dossier de destination
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
$fileService = new \FileService();
|
||||
$fileService = new FileService();
|
||||
$uploadPath = "/{$entiteId}/logo";
|
||||
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class FileController {
|
||||
|
||||
115
api/src/Controllers/HealthController.php
Normal file
115
api/src/Controllers/HealthController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use PDO;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* HealthController
|
||||
*
|
||||
* Endpoint de vérification de santé de l'API
|
||||
* Route publique pour permettre le monitoring automatique
|
||||
*/
|
||||
class HealthController
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
/**
|
||||
* Vérifie la santé de l'API
|
||||
* GET /api/health
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check(): void
|
||||
{
|
||||
$checks = [
|
||||
'api' => 'ok',
|
||||
'database' => $this->checkDatabase(),
|
||||
'directories' => $this->checkDirectories()
|
||||
];
|
||||
|
||||
// Déterminer le statut global
|
||||
$status = in_array('error', $checks, true) ? 'error' : 'ok';
|
||||
$httpCode = $status === 'ok' ? 200 : 503;
|
||||
|
||||
Response::json([
|
||||
'status' => $status,
|
||||
'checks' => $checks,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'environment' => $this->getEnvironment()
|
||||
], $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la connexion à la base de données
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDatabase(): string
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->query("SELECT 1");
|
||||
return $stmt ? 'ok' : 'error';
|
||||
} catch (Exception $e) {
|
||||
error_log("Health check database error: " . $e->getMessage());
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'accessibilité des dossiers critiques
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDirectories(): string
|
||||
{
|
||||
$basePath = __DIR__ . '/../../';
|
||||
$requiredDirs = ['logs', 'uploads'];
|
||||
|
||||
foreach ($requiredDirs as $dir) {
|
||||
$fullPath = $basePath . $dir;
|
||||
|
||||
if (!is_dir($fullPath)) {
|
||||
error_log("Health check: Directory not found: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (!is_writable($fullPath)) {
|
||||
error_log("Health check: Directory not writable: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte l'environnement actuel
|
||||
*
|
||||
* @return string 'dev', 'recette' ou 'production'
|
||||
*/
|
||||
private function getEnvironment(): string
|
||||
{
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'unknown';
|
||||
|
||||
if (str_contains($host, 'dapp.geosector.fr')) {
|
||||
return 'dev';
|
||||
} elseif (str_contains($host, 'rapp.geosector.fr')) {
|
||||
return 'recette';
|
||||
} elseif (str_contains($host, 'app3.geosector.fr') || str_contains($host, 'app.geosector.fr')) {
|
||||
return 'production';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,12 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\EventLogService;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/EntiteController.php';
|
||||
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||
@@ -55,14 +57,6 @@ class LoginController {
|
||||
// admin accessible uniquement aux fk_role>1 (admins amicale + super-admins)
|
||||
$roleCondition = ($interface === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Tentative de connexion GeoSector', [
|
||||
'level' => 'info',
|
||||
'username' => $username,
|
||||
'type' => $interface,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// Requête optimisée: on récupère l'utilisateur et son entité en une seule fois avec LEFT JOIN
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -83,11 +77,8 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'user_not_found', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'user_not_found', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
@@ -100,22 +91,15 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'invalid_password', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'invalid_password', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a une entité et si elle est active
|
||||
if (!empty($user['fk_entite']) && (!isset($user['entite_chk_active']) || $user['entite_chk_active'] != 1)) {
|
||||
LogService::log('Tentative de connexion GeoSector échouée : entité non active', [
|
||||
'level' => 'warning',
|
||||
'username' => $username,
|
||||
'entite_id' => $user['fk_entite']
|
||||
]);
|
||||
EventLogService::logLoginFailed($username, 'account_inactive', 1);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Votre amicale n\'est pas activée. Veuillez contacter votre administrateur.'
|
||||
@@ -307,16 +291,33 @@ class LoginController {
|
||||
// Récupérer l'ID de l'opération active (première opération retournée)
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// 2. Récupérer les secteurs selon l'interface et le rôle
|
||||
if ($interface === 'user') {
|
||||
// Interface utilisateur : seulement les secteurs affectés à l'utilisateur
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les secteurs distincts de l'opération
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
@@ -344,11 +345,12 @@ class LoginController {
|
||||
// 3. Récupérer les passages selon l'interface et le rôle
|
||||
if ($interface === 'user' && !empty($sectors)) {
|
||||
// Interface utilisateur : passages de l'utilisateur + passages à finaliser sur ses secteurs
|
||||
$userId = $user['id'];
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"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
|
||||
@@ -362,7 +364,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $userId, $userId]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les passages de l'opération
|
||||
@@ -423,13 +425,14 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
AND u.id != ?" // Exclure l'utilisateur connecté
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
@@ -458,14 +461,27 @@ class LoginController {
|
||||
|
||||
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
||||
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
@@ -474,6 +490,7 @@ class LoginController {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -537,13 +554,15 @@ class LoginController {
|
||||
if ($user['fk_role'] <= 2) {
|
||||
// User normal ou admin avec fk_role=2: uniquement son amicale
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -551,13 +570,15 @@ class LoginController {
|
||||
} else {
|
||||
// Admin avec fk_role>2: toutes les amicales sauf id=1
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -872,6 +893,9 @@ class LoginController {
|
||||
// Ajouter les données du chat à la réponse
|
||||
$response['chat'] = $chatData;
|
||||
|
||||
// Log de connexion réussie
|
||||
EventLogService::logLoginSuccess($user['id'], $user['fk_entite'] ?? null, $username);
|
||||
|
||||
// Envoi de la réponse
|
||||
Response::json($response);
|
||||
} catch (PDOException $e) {
|
||||
@@ -918,14 +942,6 @@ class LoginController {
|
||||
// Déterminer le roleCondition selon le mode (même logique que login)
|
||||
$roleCondition = ($mode === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Rafraîchissement session GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'mode' => $mode,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// 4. Requête pour récupérer l'utilisateur et son entité (même requête que login)
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -1074,15 +1090,32 @@ class LoginController {
|
||||
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// Récupérer les secteurs selon le mode et le rôle
|
||||
if ($mode === 'user') {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
|
||||
@@ -1106,10 +1139,12 @@ class LoginController {
|
||||
|
||||
// Récupérer les passages selon le mode et le rôle
|
||||
if ($mode === 'user' && !empty($sectors)) {
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"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
|
||||
@@ -1123,7 +1158,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $user['id'], $user['id']]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
@@ -1177,9 +1212,10 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
@@ -1209,20 +1245,34 @@ class LoginController {
|
||||
// Récupérer les membres si nécessaire
|
||||
$membresData = [];
|
||||
if ($mode === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -1279,10 +1329,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -1292,10 +1344,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -1830,13 +1884,13 @@ class LoginController {
|
||||
}
|
||||
*/
|
||||
|
||||
// 5. Vérification de l'existence du code postal dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
||||
$checkPostalStmt->execute([$postalCode]);
|
||||
// 5. Vérification de l'existence du code postal + ville dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ? AND ville = ?');
|
||||
$checkPostalStmt->execute([$postalCode, $cityName]);
|
||||
if ($checkPostalStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une amicale est déjà inscrite à ce code postal'
|
||||
'message' => 'Une amicale est déjà inscrite pour ce code postal et cette ville'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
@@ -2073,16 +2127,15 @@ class LoginController {
|
||||
// Méthodes auxiliaires
|
||||
|
||||
public function logout(): void {
|
||||
$userId = Session::getUserId() ?? null;
|
||||
$userEmail = Session::getUserEmail() ?? 'anonyme';
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
Session::logout();
|
||||
|
||||
LogService::log('Déconnexion GeoSector réussie', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'email' => $userEmail
|
||||
]);
|
||||
// Log de déconnexion
|
||||
if ($userId) {
|
||||
EventLogService::logLogout($userId, $entityId, 0);
|
||||
}
|
||||
|
||||
// Retourner une réponse standardisée
|
||||
Response::json([
|
||||
@@ -2106,12 +2159,20 @@ class LoginController {
|
||||
// Formater la ville et le code postal pour la recherche
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
// Construire l'URL de recherche pour l'API adresse.gouv.fr
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=" . urlencode($keyword) . "+$citySearch&limit=5";
|
||||
|
||||
// Effectuer la requête HTTP
|
||||
$response = @file_get_contents($searchUrl);
|
||||
// Effectuer la requête HTTP avec timeout
|
||||
$response = @file_get_contents($searchUrl, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
LogService::log('Erreur lors de la requête à l\'API adresse.gouv.fr', [
|
||||
@@ -2159,9 +2220,19 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune caserne n'a été trouvée avec les mots-clés, utiliser les coordonnées du centre de la ville
|
||||
// Si aucune caserne trouvée, chercher simplement ville + code postal avec timeout
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
$cityUrl = "https://api-adresse.data.gouv.fr/search/?q=$citySearch&limit=1";
|
||||
$cityResponse = @file_get_contents($cityUrl);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
$cityResponse = @file_get_contents($cityUrl, false, $context);
|
||||
|
||||
if ($cityResponse !== false) {
|
||||
$cityData = json_decode($cityResponse, true);
|
||||
@@ -2169,7 +2240,7 @@ class LoginController {
|
||||
if ($cityData && isset($cityData['features'][0]['geometry']['coordinates'])) {
|
||||
$coordinates = $cityData['features'][0]['geometry']['coordinates'];
|
||||
|
||||
LogService::log('Utilisation des coordonnées du centre de la ville', [
|
||||
LogService::log('Coordonnées GPS récupérées pour l\'adresse', [
|
||||
'level' => 'info',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
@@ -2183,6 +2254,12 @@ class LoginController {
|
||||
}
|
||||
|
||||
// Aucune coordonnée trouvée
|
||||
LogService::log('Aucune coordonnée GPS trouvée (timeout ou adresse invalide)', [
|
||||
'level' => 'warning',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
544
api/src/Controllers/MigrationController.php
Normal file
544
api/src/Controllers/MigrationController.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/MigrationService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\MigrationService;
|
||||
use Exception;
|
||||
|
||||
class MigrationController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
private MigrationService $migrationService;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
$this->migrationService = new MigrationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste les connexions aux bases de données source et cible
|
||||
*
|
||||
* GET /api/migrations/test-connections
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testConnections(): void {
|
||||
try {
|
||||
$result = $this->migrationService->testConnections();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'connections' => $result
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du test des connexions', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les entités disponibles à migrer depuis la base source
|
||||
*
|
||||
* GET /api/migrations/entities/available
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getAvailableEntities(): void {
|
||||
try {
|
||||
$entities = $this->migrationService->getAvailableEntities();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'count' => count($entities),
|
||||
'entities' => $entities
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des entités disponibles', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une entité source
|
||||
*
|
||||
* GET /api/migrations/entities/:id
|
||||
*
|
||||
* @param int $id ID de l'entité dans la base source
|
||||
* @return void
|
||||
*/
|
||||
public function getEntityDetails(int $id): void {
|
||||
try {
|
||||
$entity = $this->migrationService->getEntityDetails($id);
|
||||
|
||||
if (!$entity) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity' => $entity
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des détails de l\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une entité complète ou par étapes
|
||||
*
|
||||
* POST /api/migrations/entity
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "steps": ["users", "operations"], // Optionnel
|
||||
* "dry_run": false, // Optionnel
|
||||
* "truncate": false // Optionnel
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntity(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le champ entity_id est requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$steps = $data['steps'] ?? null;
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$truncate = $data['truncate'] ?? false;
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) { // 3 = admin
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'entité', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'steps' => $steps,
|
||||
'dry_run' => $dryRun,
|
||||
'truncate' => $truncate,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter la migration
|
||||
$result = $this->migrationService->migrateEntity(
|
||||
$entityId,
|
||||
$steps,
|
||||
$dryRun,
|
||||
$truncate
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'entity_name' => $result['entity_name'],
|
||||
'migration_id' => $result['migration_id'],
|
||||
'steps_completed' => $result['steps_completed'],
|
||||
'total_duration_ms' => $result['total_duration_ms'],
|
||||
'summary' => $result['summary']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une étape spécifique pour une entité
|
||||
*
|
||||
* POST /api/migrations/entity/step
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "step": "users",
|
||||
* "dry_run": false,
|
||||
* "options": {}
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntityStep(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id']) || !isset($data['step'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Les champs entity_id et step sont requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$step = $data['step'];
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$options = $data['options'] ?? [];
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'étape', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'dry_run' => $dryRun,
|
||||
'options' => $options,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter l'étape de migration
|
||||
$result = $this->migrationService->migrateStep(
|
||||
$entityId,
|
||||
$step,
|
||||
$dryRun,
|
||||
$options
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => $result['duration_ms'],
|
||||
'warnings' => $result['warnings'] ?? [],
|
||||
'details' => $result['details'] ?? []
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null,
|
||||
'step' => $data['step'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/status
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationStatus(int $id): void {
|
||||
try {
|
||||
$status = $this->migrationService->getMigrationStatus($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'migration_status' => $status
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération du statut de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les logs de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/logs
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationLogs(int $id): void {
|
||||
try {
|
||||
$logs = $this->migrationService->getMigrationLogs($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'logs' => $logs
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des logs de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport de migration pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/report
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationReport(int $id): void {
|
||||
try {
|
||||
$report = $this->migrationService->generateMigrationReport($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'report' => $report
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la génération du rapport de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare les données source vs cible pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/compare
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function compareEntityData(int $id): void {
|
||||
try {
|
||||
$comparison = $this->migrationService->compareEntityData($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'comparison' => $comparison
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la comparaison des données', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'intégrité des données migrées pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/verify
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function verifyMigration(int $id): void {
|
||||
try {
|
||||
$verification = $this->migrationService->verifyMigration($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'verification' => $verification
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la migration d'une entité (rollback)
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackEntity(int $id): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'entité', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackEntity($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'message' => 'Migration annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une étape spécifique de la migration
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id/step/:step
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @param string $step Nom de l'étape
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackStep(int $id, string $step): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'étape', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackStep($id, $step);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'message' => 'Étape annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de l\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id,
|
||||
'step' => $step
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ExportService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/OperationDataService.php';
|
||||
@@ -16,10 +17,11 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ExportService;
|
||||
use ApiService;
|
||||
use OperationDataService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ExportService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\OperationDataService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -378,34 +380,37 @@ class OperationController {
|
||||
$newSectId = (int)$this->db->lastInsertId();
|
||||
$duplicatedSectors++;
|
||||
|
||||
// Étape 4.3 : Dupliquer les users_sectors en vérifiant que fk_user existe dans ope_users
|
||||
// Étape 4.3 : Dupliquer les users_sectors en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, fk_user_creat)
|
||||
SELECT ?, ous.fk_user, ?, ?
|
||||
SELECT ?, new_ou.id, ?, ?
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN ope_users ou ON ou.fk_user = ous.fk_user AND ou.fk_operation = ?
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = ous.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE ous.fk_operation = ? AND ous.fk_sector = ? AND ous.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedUsersSectors += $stmt->rowCount();
|
||||
|
||||
// Étape 4.4 : Dupliquer les passages avec les valeurs par défaut spécifiées
|
||||
// Étape 4.4 : Dupliquer les passages en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_user_creat, chk_active
|
||||
)
|
||||
SELECT
|
||||
?, ?, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 1, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ? AND fk_sector = ? AND chk_active = 1
|
||||
SELECT
|
||||
?, ?, new_ou.id, op.fk_adresse, op.numero, op.rue, op.rue_bis, op.ville,
|
||||
op.fk_habitat, op.appt, op.niveau, op.residence, op.gps_lat, op.gps_lng, op.encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass op
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = op.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE op.fk_operation = ? AND op.fk_sector = ? AND op.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedPassages += $stmt->rowCount();
|
||||
}
|
||||
|
||||
@@ -455,19 +460,12 @@ class OperationController {
|
||||
// Étape 7 : Préparer la réponse avec les groupes JSON
|
||||
$response = OperationDataService::prepareOperationResponse($this->db, $newOpeId, $entiteId);
|
||||
|
||||
LogService::log('Création opération terminée avec succès', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'newOpeId' => $newOpeId,
|
||||
'oldOpeId' => $oldOpeId,
|
||||
'stats' => [
|
||||
'insertedUsers' => $insertedUsers,
|
||||
'duplicatedSectors' => $duplicatedSectors,
|
||||
'duplicatedUsersSectors' => $duplicatedUsersSectors,
|
||||
'duplicatedPassages' => $duplicatedPassages
|
||||
]
|
||||
]);
|
||||
// Log de création de l'opération
|
||||
EventLogService::logOperationCreated(
|
||||
$newOpeId,
|
||||
$data['date_deb'],
|
||||
$data['date_fin']
|
||||
);
|
||||
|
||||
Response::json($response, 201);
|
||||
} catch (Exception $e) {
|
||||
@@ -621,12 +619,24 @@ class OperationController {
|
||||
$operationId
|
||||
]);
|
||||
|
||||
LogService::log('Mise à jour d\'une opération', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de mise à jour de l'opération
|
||||
$changes = [];
|
||||
if (isset($data['libelle']) || isset($data['name'])) {
|
||||
$changes['libelle'] = ['new' => $libelle];
|
||||
}
|
||||
if (isset($data['date_deb'])) {
|
||||
$changes['date_deb'] = ['new' => $data['date_deb']];
|
||||
}
|
||||
if (isset($data['date_fin'])) {
|
||||
$changes['date_fin'] = ['new' => $data['date_fin']];
|
||||
}
|
||||
if (isset($data['chk_distinct_sectors'])) {
|
||||
$changes['chk_distinct_sectors'] = ['new' => (int)$data['chk_distinct_sectors']];
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logOperationUpdated($operationId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -820,25 +830,8 @@ class OperationController {
|
||||
// Valider la transaction
|
||||
$this->db->commit();
|
||||
|
||||
LogService::log('Suppression complète d\'une opération et de toutes ses données', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'userRole' => $userRole,
|
||||
'userEntiteId' => $userEntiteId,
|
||||
'operationEntiteId' => $operationEntiteId,
|
||||
'operationId' => $operationId,
|
||||
'operationActive' => $operationActive,
|
||||
'deletedCounts' => [
|
||||
'medias' => $deletedMedias,
|
||||
'ope_pass_histo' => $deletedPassHisto,
|
||||
'ope_pass' => $deletedPass,
|
||||
'ope_users_sectors' => $deletedUsersSectors,
|
||||
'sectors_adresses' => $deletedSectorsAdresses,
|
||||
'ope_sectors' => $deletedSectors,
|
||||
'ope_users' => $deletedUsers,
|
||||
'operations' => 1
|
||||
]
|
||||
]);
|
||||
// Log de suppression de l'opération (suppression physique)
|
||||
EventLogService::logOperationDeleted($operationId, false);
|
||||
|
||||
// Préparer la réponse selon le statut de l'opération supprimée
|
||||
$response = [
|
||||
@@ -948,13 +941,14 @@ class OperationController {
|
||||
|
||||
// Récupérer les relations utilisateurs-secteurs
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
SELECT
|
||||
ous.id, ous.fk_operation, ous.fk_user, ous.fk_sector,
|
||||
ous.created_at, ous.updated_at, ous.chk_active,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name,
|
||||
s.libelle as sector_name
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN users u ON u.id = ous.fk_user
|
||||
INNER JOIN ope_users ou ON ou.id = ous.fk_user
|
||||
INNER JOIN users u ON u.id = ou.fk_user
|
||||
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||
ORDER BY s.libelle, u.encrypted_name
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -233,13 +235,14 @@ class PassageController {
|
||||
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.stripe_payment_link_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE $whereClause AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -324,13 +327,14 @@ class PassageController {
|
||||
$passageId = (int)$id;
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
p.*,
|
||||
SELECT
|
||||
p.*,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
');
|
||||
|
||||
@@ -410,12 +414,13 @@ class PassageController {
|
||||
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
|
||||
p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.chk_email_sent,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.stripe_payment_link_id, p.chk_email_sent,
|
||||
p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
|
||||
p.anomalie, p.created_at, p.updated_at,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -510,6 +515,24 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur du passage
|
||||
// $data['fk_user'] contient users.id, on doit le convertir en ope_users.id
|
||||
$passageUserId = (int)$data['fk_user'];
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$passageUserId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = '';
|
||||
if (isset($data['name']) && !empty(trim($data['name']))) {
|
||||
@@ -527,7 +550,7 @@ class PassageController {
|
||||
$insertData = [
|
||||
'fk_operation' => $operationId,
|
||||
'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0,
|
||||
'fk_user' => (int)$data['fk_user'],
|
||||
'fk_user' => $opeUserId,
|
||||
'fk_adresse' => $data['fk_adresse'] ?? '',
|
||||
'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
|
||||
'fk_type' => isset($data['fk_type']) ? (int)$data['fk_type'] : 0,
|
||||
@@ -569,12 +592,14 @@ class PassageController {
|
||||
|
||||
$passageId = $this->db->lastInsertId();
|
||||
|
||||
LogService::log('Création d\'un nouveau passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de création du passage
|
||||
EventLogService::logPassageCreated(
|
||||
(int)$passageId,
|
||||
$insertData['fk_operation'],
|
||||
$insertData['fk_sector'],
|
||||
$insertData['montant'],
|
||||
(string)$insertData['fk_type_reglement']
|
||||
);
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -702,16 +727,33 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur connecté
|
||||
$operationId = $passage['fk_operation'];
|
||||
$stmtCurrentOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCurrentOpeUser->execute([$userId, $operationId]);
|
||||
$currentOpeUserId = $stmtCurrentOpeUser->fetchColumn();
|
||||
|
||||
if (!$currentOpeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur connecté non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
|
||||
// On force l'attribution du passage à l'utilisateur actuel
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $userId) {
|
||||
$data['fk_user'] = $userId;
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $currentOpeUserId) {
|
||||
$data['fk_user'] = $currentOpeUserId;
|
||||
|
||||
LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'ancien_user' => $passage['fk_user'],
|
||||
'nouveau_user' => $userId
|
||||
'nouveau_user' => $currentOpeUserId
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -722,7 +764,7 @@ class PassageController {
|
||||
// Champs pouvant être mis à jour
|
||||
$updatableFields = [
|
||||
'fk_sector',
|
||||
'fk_user',
|
||||
// Note: fk_user est traité séparément pour conversion users.id -> ope_users.id
|
||||
'fk_adresse',
|
||||
'passed_at',
|
||||
'fk_type',
|
||||
@@ -740,6 +782,7 @@ class PassageController {
|
||||
'fk_type_reglement',
|
||||
'remarque',
|
||||
'stripe_payment_id',
|
||||
'stripe_payment_link_id',
|
||||
'nom_recu',
|
||||
'date_recu',
|
||||
'docremis',
|
||||
@@ -756,6 +799,48 @@ class PassageController {
|
||||
}
|
||||
}
|
||||
|
||||
// Traitement spécial pour fk_user : conversion users.id -> ope_users.id
|
||||
if (isset($data['fk_user'])) {
|
||||
// Si $data['fk_user'] vient de l'attribution automatique, c'est déjà ope_users.id
|
||||
// Sinon, on doit convertir users.id en ope_users.id
|
||||
$providedUserId = (int)$data['fk_user'];
|
||||
|
||||
// Vérifier si c'est déjà un ope_users.id valide
|
||||
$stmtCheckOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCheckOpeUser->execute([$providedUserId, $operationId]);
|
||||
$isOpeUserId = $stmtCheckOpeUser->fetchColumn();
|
||||
|
||||
if ($isOpeUserId) {
|
||||
// C'est déjà un ope_users.id valide
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $providedUserId;
|
||||
} else {
|
||||
// C'est probablement un users.id, on le convertit
|
||||
$stmtGetOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtGetOpeUser->execute([$providedUserId, $operationId]);
|
||||
$convertedOpeUserId = $stmtGetOpeUser->fetchColumn();
|
||||
|
||||
if ($convertedOpeUserId) {
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $convertedOpeUserId;
|
||||
} else {
|
||||
// Utilisateur non trouvé, on ignore cette mise à jour
|
||||
LogService::log('Tentative de mise à jour avec un utilisateur invalide', [
|
||||
'level' => 'warning',
|
||||
'passageId' => $passageId,
|
||||
'provided_user_id' => $providedUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion des champs chiffrés
|
||||
if (array_key_exists('name', $data)) {
|
||||
$updateFields[] = "encrypted_name = ?";
|
||||
@@ -791,11 +876,21 @@ class PassageController {
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
LogService::log('Mise à jour d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de mise à jour du passage (changements simplifiés)
|
||||
$changes = [];
|
||||
foreach ($data as $key => $value) {
|
||||
// Ne logger que les champs non sensibles
|
||||
if (!in_array($key, ['name', 'email', 'phone', 'encrypted_name', 'encrypted_email', 'encrypted_phone'])) {
|
||||
$changes[$key] = ['new' => $value];
|
||||
} else {
|
||||
// Indiquer qu'un champ chiffré a été modifié
|
||||
$changes[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logPassageUpdated((int)$passageId, $changes);
|
||||
}
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -944,7 +1039,7 @@ class PassageController {
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.id
|
||||
SELECT p.id, p.fk_operation
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
@@ -962,18 +1057,19 @@ class PassageController {
|
||||
|
||||
// Désactiver le passage (soft delete)
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
UPDATE ope_pass
|
||||
SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
|
||||
$stmt->execute([$userId, $passageId]);
|
||||
|
||||
LogService::log('Suppression d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de suppression du passage
|
||||
EventLogService::logPassageDeleted(
|
||||
$passageId,
|
||||
(int)$passage['fk_operation'],
|
||||
true // soft delete
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -9,7 +9,7 @@ require_once __DIR__ . '/../Services/LogService.php';
|
||||
|
||||
use Request;
|
||||
use Response;
|
||||
use LogService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,14 +3,14 @@ namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use AddressService;
|
||||
use DepartmentBoundaryService;
|
||||
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';
|
||||
require_once __DIR__ . '/../Services/AddressService.php';
|
||||
require_once __DIR__ . '/../Services/DepartmentBoundaryService.php';
|
||||
|
||||
class SectorController
|
||||
{
|
||||
@@ -193,14 +193,31 @@ class SectorController
|
||||
|
||||
// Affectation des users si fournis
|
||||
if (!empty($users)) {
|
||||
$queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$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) {
|
||||
$this->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' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
@@ -268,16 +285,24 @@ class SectorController
|
||||
$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)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$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'],
|
||||
@@ -287,60 +312,111 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'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)) {
|
||||
$firstUserId = $users[0]; // Premier user pour l'affectation des passages
|
||||
$passageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville,
|
||||
gps_lat, gps_lng, fk_type, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville,
|
||||
:gps_lat, :gps_lng, 2, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$passageStmt = $this->db->prepare($passageQuery);
|
||||
|
||||
$passagesCreated = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin
|
||||
if (in_array($address['id'], $addressesToExclude)) {
|
||||
continue; // Passer à l'adresse suivante
|
||||
}
|
||||
|
||||
try {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
$passageStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstUserId,
|
||||
'fk_adresse' => $address['id'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['voie'],
|
||||
'rue_bis' => $rueBis,
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude'],
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
$passagesCreated++;
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// 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) {
|
||||
$this->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) {
|
||||
$this->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) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@@ -351,9 +427,16 @@ class SectorController
|
||||
'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
|
||||
@@ -413,9 +496,10 @@ class SectorController
|
||||
}
|
||||
|
||||
// Récupérer les users affectés
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$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 users u ON ous.fk_user = u.id
|
||||
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]);
|
||||
@@ -425,7 +509,8 @@ class SectorController
|
||||
$responseData['users_sectors'] = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'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'],
|
||||
@@ -498,24 +583,27 @@ class SectorController
|
||||
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
|
||||
$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]);
|
||||
|
||||
if (!$checkStmt->fetch()) {
|
||||
|
||||
$existingSector = $checkStmt->fetch();
|
||||
if (!$existingSector) {
|
||||
Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $existingSector['fk_operation'];
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
@@ -580,8 +668,8 @@ class SectorController
|
||||
|
||||
// Ajouter les nouvelles affectations
|
||||
if (!empty($data['users'])) {
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$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)";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [
|
||||
'query' => $insertQuery
|
||||
]);
|
||||
@@ -591,9 +679,27 @@ class SectorController
|
||||
$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) {
|
||||
$this->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' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $id,
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
];
|
||||
@@ -626,14 +732,25 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer les passages si le secteur a changé
|
||||
// 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
|
||||
];
|
||||
if (isset($data['sector'])) {
|
||||
|
||||
// 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) {
|
||||
$this->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
|
||||
@@ -660,17 +777,25 @@ class SectorController
|
||||
]);
|
||||
|
||||
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);
|
||||
|
||||
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
$this->logService->info('[UPDATE] Adresses récupérées', [
|
||||
'sector_id' => $id,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
if (!empty($addresses)) {
|
||||
$queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$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,
|
||||
@@ -680,7 +805,13 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'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
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -715,10 +846,29 @@ class SectorController
|
||||
|
||||
// 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
|
||||
SELECT
|
||||
s.id,
|
||||
s.libelle,
|
||||
s.color,
|
||||
@@ -726,57 +876,61 @@ class SectorController
|
||||
FROM ope_sectors s
|
||||
WHERE s.id = :id
|
||||
";
|
||||
|
||||
|
||||
$stmt = $this->db->prepare($query);
|
||||
$stmt->execute(['id' => $id]);
|
||||
$sector = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupérer tous les passages du secteur
|
||||
$passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at,
|
||||
numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email,
|
||||
encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
ORDER BY id";
|
||||
$passagesStmt = $this->db->prepare($passagesQuery);
|
||||
$passagesStmt->execute(['sector_id' => $id]);
|
||||
$passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
|
||||
// Récupérer les passages UNIQUEMENT si chk_adresses_change = 1
|
||||
$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;
|
||||
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;
|
||||
}
|
||||
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, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$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 users u ON ous.fk_user = u.id
|
||||
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";
|
||||
|
||||
@@ -801,7 +955,8 @@ class SectorController
|
||||
$usersDecrypted = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'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'],
|
||||
@@ -934,18 +1089,20 @@ class SectorController
|
||||
}
|
||||
|
||||
// Vérifier que le secteur existe et récupérer ses informations
|
||||
$checkQuery = "SELECT s.id, s.libelle, o.fk_entite
|
||||
$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();
|
||||
|
||||
@@ -1001,9 +1158,16 @@ class SectorController
|
||||
$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) {
|
||||
@@ -1249,8 +1413,11 @@ class SectorController
|
||||
}
|
||||
|
||||
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE)
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses
|
||||
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
// 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();
|
||||
@@ -1268,93 +1435,121 @@ class SectorController
|
||||
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
|
||||
|
||||
if ($firstUserId && !empty($addresses)) {
|
||||
$this->logService->info('[updatePassagesForSector] Optimisation passages', [
|
||||
$this->logService->info('[updatePassagesForSector] Traitement des passages', [
|
||||
'user_id' => $firstUserId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
// OPTIMISATION : Récupérer TOUS les passages existants en UNE requête
|
||||
$addressIds = array_filter(array_column($addresses, 'fk_adresse'));
|
||||
|
||||
// Construire la requête pour récupérer tous les passages existants
|
||||
// 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
|
||||
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
|
||||
AND (";
|
||||
|
||||
$params = ['operation_id' => $operationId];
|
||||
$conditions = [];
|
||||
|
||||
// Condition pour les fk_adresse
|
||||
if (!empty($addressIds)) {
|
||||
$placeholders = [];
|
||||
foreach ($addressIds as $idx => $addrId) {
|
||||
$key = 'addr_' . $idx;
|
||||
$placeholders[] = ':' . $key;
|
||||
$params[$key] = $addrId;
|
||||
}
|
||||
$conditions[] = "fk_adresse IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
// Condition pour les données d'adresse (numero, rue, ville)
|
||||
$addressConditions = [];
|
||||
foreach ($addresses as $idx => $addr) {
|
||||
$numKey = 'num_' . $idx;
|
||||
$rueKey = 'rue_' . $idx;
|
||||
$bisKey = 'bis_' . $idx;
|
||||
$villeKey = 'ville_' . $idx;
|
||||
|
||||
$addressConditions[] = "(numero = :$numKey AND rue = :$rueKey AND rue_bis = :$bisKey AND ville = :$villeKey)";
|
||||
$params[$numKey] = $addr['numero'];
|
||||
$params[$rueKey] = $addr['rue'];
|
||||
$params[$bisKey] = $addr['rue_bis'];
|
||||
$params[$villeKey] = $addr['ville'];
|
||||
}
|
||||
|
||||
if (!empty($addressConditions)) {
|
||||
$conditions[] = "(" . implode(' OR ', $addressConditions) . ")";
|
||||
}
|
||||
|
||||
$existingQuery .= implode(' OR ', $conditions) . ")";
|
||||
WHERE fk_operation = :operation_id";
|
||||
|
||||
$existingStmt = $this->db->prepare($existingQuery);
|
||||
$existingStmt->execute($params);
|
||||
$existingStmt->execute(['operation_id' => $operationId]);
|
||||
$existingPassages = $existingStmt->fetchAll();
|
||||
|
||||
// Indexer les passages existants pour recherche rapide
|
||||
// Indexer les passages existants par clé : numero|rue|rue_bis|ville
|
||||
$passagesByAddress = [];
|
||||
$passagesByData = [];
|
||||
foreach ($existingPassages as $p) {
|
||||
if (!empty($p['fk_adresse'])) {
|
||||
$passagesByAddress[$p['fk_adresse']] = $p;
|
||||
$addressKey = $p['numero'] . '|' . $p['rue'] . '|' . ($p['rue_bis'] ?? '') . '|' . $p['ville'];
|
||||
if (!isset($passagesByAddress[$addressKey])) {
|
||||
$passagesByAddress[$addressKey] = [];
|
||||
}
|
||||
$dataKey = $p['numero'] . '|' . $p['rue'] . '|' . $p['rue_bis'] . '|' . $p['ville'];
|
||||
$passagesByData[$dataKey] = $p;
|
||||
$passagesByAddress[$addressKey][] = $p;
|
||||
}
|
||||
|
||||
// Préparer les listes pour batch insert/update
|
||||
// Traiter chaque adresse du secteur
|
||||
$toInsert = [];
|
||||
$toUpdate = [];
|
||||
$toDelete = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Vérification en mémoire PHP (0 requête)
|
||||
if (!empty($address['fk_adresse']) && isset($passagesByAddress[$address['fk_adresse']])) {
|
||||
continue; // Déjà existant avec bon fk_adresse
|
||||
}
|
||||
$addressKey = $address['numero'] . '|' . $address['rue'] . '|' . ($address['rue_bis'] ?? '') . '|' . $address['ville'];
|
||||
$existingAtAddress = $passagesByAddress[$addressKey] ?? [];
|
||||
$nbExisting = count($existingAtAddress);
|
||||
|
||||
$dataKey = $address['numero'] . '|' . $address['rue'] . '|' . $address['rue_bis'] . '|' . $address['ville'];
|
||||
if (isset($passagesByData[$dataKey])) {
|
||||
// Passage existant mais sans fk_adresse ou avec fk_adresse différent
|
||||
if (!empty($address['fk_adresse']) && $passagesByData[$dataKey]['fk_adresse'] != $address['fk_adresse']) {
|
||||
$toUpdate[] = [
|
||||
'id' => $passagesByData[$dataKey]['id'],
|
||||
'fk_adresse' => $address['fk_adresse']
|
||||
$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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nouveau passage à créer
|
||||
$toInsert[] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,19 +1559,24 @@ class SectorController
|
||||
$insertParams = [];
|
||||
$paramIndex = 0;
|
||||
|
||||
foreach ($toInsert as $addr) {
|
||||
foreach ($toInsert as $item) {
|
||||
$addr = $item['address'];
|
||||
$values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex,
|
||||
:num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, '', NOW(), :creat$paramIndex, 1)";
|
||||
: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'];
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'] ?? null;
|
||||
$insertParams["num$paramIndex"] = $addr['numero'];
|
||||
$insertParams["rue$paramIndex"] = $addr['rue'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'];
|
||||
$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;
|
||||
@@ -1386,7 +1586,7 @@ class SectorController
|
||||
|
||||
$insertQuery = "INSERT INTO ope_pass
|
||||
(fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis,
|
||||
ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
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 {
|
||||
@@ -1401,28 +1601,67 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE MULTIPLE avec CASE WHEN
|
||||
// UPDATE MULTIPLE avec CASE WHEN (inclut GPS pour uniformisation)
|
||||
if (!empty($toUpdate)) {
|
||||
$updateIds = array_column($toUpdate, 'id');
|
||||
$placeholders = str_repeat('?,', count($updateIds) - 1) . '?';
|
||||
|
||||
$caseWhen = [];
|
||||
$caseWhenHabitat = [];
|
||||
$caseWhenResidence = [];
|
||||
$caseWhenGpsLat = [];
|
||||
$caseWhenGpsLng = [];
|
||||
$updateParams = [];
|
||||
|
||||
foreach ($toUpdate as $upd) {
|
||||
$caseWhen[] = "WHEN id = ? THEN ?";
|
||||
// fk_habitat est toujours présent
|
||||
$caseWhenHabitat[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['fk_adresse'];
|
||||
$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 fk_adresse = CASE " . implode(' ', $caseWhen) . " END
|
||||
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)
|
||||
$this->logService->info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [
|
||||
'nb_updated' => count($toUpdate),
|
||||
'sector_id' => $sectorId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la mise à jour multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
@@ -1431,6 +1670,23 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$this->logService->error('Erreur lors de la suppression multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Services\StripeService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\FileService;
|
||||
use App\Services\ApiService;
|
||||
use Session;
|
||||
use Exception;
|
||||
|
||||
@@ -77,7 +80,7 @@ class StripeController extends Controller {
|
||||
$this->requireAuth();
|
||||
|
||||
// Log du début de la requête
|
||||
\LogService::log('Début createOnboardingLink', [
|
||||
LogService::log('Début createOnboardingLink', [
|
||||
'account_id' => $accountId,
|
||||
'user_id' => Session::getUserId()
|
||||
]);
|
||||
@@ -98,7 +101,7 @@ class StripeController extends Controller {
|
||||
$returnUrl = $data['return_url'] ?? '';
|
||||
$refreshUrl = $data['refresh_url'] ?? '';
|
||||
|
||||
\LogService::log('URLs reçues', [
|
||||
LogService::log('URLs reçues', [
|
||||
'return_url' => $returnUrl,
|
||||
'refresh_url' => $refreshUrl
|
||||
]);
|
||||
@@ -110,7 +113,7 @@ class StripeController extends Controller {
|
||||
|
||||
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
|
||||
|
||||
\LogService::log('Résultat createOnboardingLink', [
|
||||
LogService::log('Résultat createOnboardingLink', [
|
||||
'success' => $result['success'] ?? false,
|
||||
'has_url' => isset($result['url'])
|
||||
]);
|
||||
@@ -127,7 +130,7 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
\LogService::log('Erreur createOnboardingLink', [
|
||||
LogService::log('Erreur createOnboardingLink', [
|
||||
'level' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
@@ -190,7 +193,7 @@ class StripeController extends Controller {
|
||||
|
||||
// Vérifier que le passage existe et appartient à l'utilisateur
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite
|
||||
SELECT p.*, o.fk_entite, o.id as operation_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND p.fk_user = ?
|
||||
@@ -210,13 +213,15 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)($passage['montant'] * 100);
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = $passage['fk_entite'];
|
||||
$operationId = $passage['operation_id'];
|
||||
$fkUser = $passage['fk_user']; // ope_users.id
|
||||
|
||||
// Déterminer le type de paiement (Tap to Pay ou Web)
|
||||
$paymentMethodTypes = $data['payment_method_types'] ?? ['card_present'];
|
||||
@@ -230,14 +235,16 @@ class StripeController extends Controller {
|
||||
'payment_method_types' => $paymentMethodTypes,
|
||||
'capture_method' => $data['capture_method'] ?? 'automatic',
|
||||
'passage_id' => $passageId,
|
||||
'amicale_id' => $data['amicale_id'] ?? $entiteId,
|
||||
'member_id' => $data['member_id'] ?? Session::getUserId(),
|
||||
'fk_entite' => $data['amicale_id'] ?? $entiteId,
|
||||
'fk_user' => $data['member_id'] ?? $fkUser,
|
||||
'stripe_account' => $data['stripe_account'] ?? null,
|
||||
'metadata' => array_merge(
|
||||
[
|
||||
'passage_id' => (string)$passageId,
|
||||
'operation_id' => (string)$operationId,
|
||||
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
||||
'member_id' => (string)($data['member_id'] ?? Session::getUserId()),
|
||||
'fk_user' => (string)$fkUser,
|
||||
'created_at' => (string)time(),
|
||||
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
||||
],
|
||||
$data['metadata'] ?? []
|
||||
@@ -291,11 +298,12 @@ class StripeController extends Controller {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite,
|
||||
e.encrypted_name as entite_nom,
|
||||
u.first_name as user_prenom, u.sect_name as user_nom
|
||||
ou.first_name as user_prenom, u.sect_name as user_nom
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
LEFT JOIN entites e ON o.fk_entite = e.id
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
||||
LEFT JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntentId]);
|
||||
@@ -330,7 +338,7 @@ class StripeController extends Controller {
|
||||
$entiteNom = '';
|
||||
if (!empty($passage['entite_nom'])) {
|
||||
try {
|
||||
$entiteNom = \ApiService::decryptData($passage['entite_nom']);
|
||||
$entiteNom = ApiService::decryptData($passage['entite_nom']);
|
||||
} catch (Exception $e) {
|
||||
$entiteNom = 'Entité inconnue';
|
||||
}
|
||||
@@ -400,6 +408,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => false,
|
||||
'account_id' => null,
|
||||
'location_id' => null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false
|
||||
@@ -415,6 +424,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false,
|
||||
@@ -440,6 +450,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => $stripeAccount->charges_enabled,
|
||||
'payouts_enabled' => $stripeAccount->payouts_enabled,
|
||||
'onboarding_completed' => $stripeAccount->details_submitted,
|
||||
@@ -529,17 +540,17 @@ class StripeController extends Controller {
|
||||
public function getPublicConfig(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
|
||||
$this->sendSuccess([
|
||||
'public_key' => $this->stripeService->getPublicKey(),
|
||||
'test_mode' => $this->stripeService->isTestMode()
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/stripe/stats
|
||||
* Récupérer les statistiques de paiement
|
||||
@@ -613,9 +624,164 @@ class StripeController extends Controller {
|
||||
'to' => $dateTo
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/payment-links
|
||||
* Créer un Payment Link Stripe pour paiement par QR Code
|
||||
*
|
||||
* Payload:
|
||||
* {
|
||||
* "amount": 2500,
|
||||
* "currency": "eur",
|
||||
* "description": "Calendrier pompiers",
|
||||
* "passage_id": 789,
|
||||
* "metadata": {...}
|
||||
* }
|
||||
*/
|
||||
public function createPaymentLink(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
||||
$this->sendError('Montant et passage_id requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$amount = (int)$data['amount'];
|
||||
$passageId = (int)$data['passage_id'];
|
||||
|
||||
// Validation du montant (doit être > 0)
|
||||
if ($amount <= 0) {
|
||||
$this->sendError('Le montant doit être supérieur à 0', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le passage appartient à l'utilisateur ou à son entité
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
JOIN ope_users ou ON p.fk_user = ou.id
|
||||
WHERE p.id = ?
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Passage non trouvé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits : soit l'utilisateur est le créateur du passage, soit il appartient à la même entité
|
||||
$userEntityId = Session::getEntityId();
|
||||
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
||||
$this->sendError('Passage non autorisé', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas déjà un paiement ou un payment link pour ce passage
|
||||
if (!empty($passage['stripe_payment_id'])) {
|
||||
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($passage['stripe_payment_link_id'])) {
|
||||
$this->sendError('Un Payment Link existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer les paramètres
|
||||
$params = [
|
||||
'amount' => $amount,
|
||||
'currency' => $data['currency'] ?? 'eur',
|
||||
'description' => $data['description'] ?? 'Calendrier pompiers',
|
||||
'passage_id' => $passageId,
|
||||
'metadata' => $data['metadata'] ?? []
|
||||
];
|
||||
|
||||
// Créer le Payment Link
|
||||
$result = $this->stripeService->createPaymentLink($params);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'payment_link_id' => $result['payment_link_id'],
|
||||
'url' => $result['url'],
|
||||
'amount' => $result['amount'],
|
||||
'passage_id' => $passageId,
|
||||
'type' => 'qr_code'
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/locations
|
||||
* Créer une Location Stripe Terminal pour une entité (nécessaire pour Tap to Pay)
|
||||
*/
|
||||
public function createLocation(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
// Vérifier le rôle de l'utilisateur
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch();
|
||||
$userRole = $result ? (int)$result['fk_role'] : 0;
|
||||
|
||||
if ($userRole < 2) {
|
||||
$this->sendError('Droits insuffisants - Admin amicale minimum requis', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
||||
|
||||
if (!$entiteId) {
|
||||
$this->sendError('ID entité requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits sur cette entité
|
||||
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
||||
$this->sendError('Non autorisé pour cette entité', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->createLocation($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'location_id' => $result['location_id'],
|
||||
'message' => $result['message']
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur lors de la création de la location: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class StripeWebhookController extends Controller {
|
||||
}
|
||||
|
||||
// Récupérer le secret webhook selon le mode
|
||||
$stripeConfig = $this->config->get('stripe');
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
$stripeConfig = $this->config->getStripeConfig();
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
? $stripeConfig['webhook_secret_test']
|
||||
: $stripeConfig['webhook_secret_live'];
|
||||
|
||||
@@ -95,31 +95,35 @@ class StripeWebhookController extends Controller {
|
||||
case 'account.updated':
|
||||
$this->handleAccountUpdated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'account.application.authorized':
|
||||
$this->handleAccountAuthorized($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
$this->handlePaymentIntentSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
$this->handlePaymentIntentFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'checkout.session.completed':
|
||||
$this->handleCheckoutSessionCompleted($event->data->object);
|
||||
break;
|
||||
|
||||
case 'charge.dispute.created':
|
||||
$this->handleChargeDisputeCreated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_succeeded':
|
||||
$this->handleTerminalReaderActionSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_failed':
|
||||
$this->handleTerminalReaderActionFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
// Événement non géré mais valide
|
||||
error_log("Unhandled Stripe event type: {$event->type}");
|
||||
@@ -278,7 +282,60 @@ class StripeWebhookController extends Controller {
|
||||
|
||||
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gérer la complétion d'une session de paiement (Payment Link / Checkout)
|
||||
*/
|
||||
private function handleCheckoutSessionCompleted($session): void {
|
||||
$metadata = $session->metadata;
|
||||
|
||||
// Logger l'événement
|
||||
error_log("Checkout session completed: {$session->id}, payment_intent: {$session->payment_intent}");
|
||||
|
||||
// Vérifier si un passage_id est présent dans les metadata
|
||||
if (isset($metadata->passage_id) && !empty($metadata->passage_id)) {
|
||||
$passageId = (int)$metadata->passage_id;
|
||||
|
||||
// Mettre à jour le passage avec le stripe_payment_id
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = :payment_intent_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute([
|
||||
'payment_intent_id' => $session->payment_intent,
|
||||
'passage_id' => $passageId
|
||||
]);
|
||||
|
||||
// Vérifier si la mise à jour a réussi
|
||||
if ($stmt->rowCount() > 0) {
|
||||
error_log("Passage {$passageId} updated with payment_intent {$session->payment_intent}");
|
||||
|
||||
// TODO: Envoyer un email de confirmation avec le reçu fiscal
|
||||
// TODO: Mettre à jour les statistiques en temps réel
|
||||
} else {
|
||||
error_log("Warning: Passage {$passageId} not found or already updated");
|
||||
}
|
||||
} else {
|
||||
error_log("Warning: checkout.session.completed without passage_id in metadata");
|
||||
}
|
||||
|
||||
// Enregistrer l'historique de la session dans stripe_payment_history si nécessaire
|
||||
if (isset($metadata->passage_id)) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM ope_pass WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute(['passage_id' => $metadata->passage_id]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if ($passage) {
|
||||
// Log dans l'historique
|
||||
error_log("Checkout session completed for passage {$metadata->passage_id}: amount={$session->amount_total}, currency={$session->currency}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer un litige (chargeback)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
class UserController {
|
||||
@@ -529,16 +531,13 @@ class UserController {
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => !empty($email) ? $email : 'non fourni',
|
||||
'username' => $username,
|
||||
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
|
||||
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
|
||||
'emailsSent' => !empty($email) ? '2 emails (username + password)' : 'aucun (pas d\'email)'
|
||||
]);
|
||||
// Log de création utilisateur
|
||||
EventLogService::logUserCreated(
|
||||
(int)$userId,
|
||||
(int)$entiteId,
|
||||
(int)$role,
|
||||
$username
|
||||
);
|
||||
|
||||
// Préparer la réponse avec les informations de connexion si générées automatiquement
|
||||
$responseData = [
|
||||
@@ -762,12 +761,23 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector mis à jour', [
|
||||
'level' => 'info',
|
||||
'modifiedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'fields' => array_keys($data),
|
||||
]);
|
||||
// Log de mise à jour utilisateur
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile', 'username', 'password'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logUserUpdated((int)$id, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -858,24 +868,72 @@ class UserController {
|
||||
|
||||
if ($transferTo) {
|
||||
try {
|
||||
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
|
||||
$stmt3 = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_user_id
|
||||
WHERE fk_user = :delete_user_id
|
||||
// Transférer les passages opération par opération
|
||||
// (car fk_user dans ope_pass pointe vers ope_users.id, pas users.id)
|
||||
|
||||
// Récupérer toutes les opérations où l'utilisateur à supprimer a des entrées dans ope_users
|
||||
$stmtOps = $this->db->prepare('
|
||||
SELECT DISTINCT fk_operation
|
||||
FROM ope_users
|
||||
WHERE fk_user = ?
|
||||
');
|
||||
$stmt3->execute([
|
||||
'new_user_id' => $transferTo,
|
||||
'delete_user_id' => $id
|
||||
]);
|
||||
|
||||
$transferredCount = $stmt3->rowCount();
|
||||
|
||||
$stmtOps->execute([$id]);
|
||||
$operations = $stmtOps->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$totalTransferred = 0;
|
||||
|
||||
foreach ($operations as $operationId) {
|
||||
// Trouver ope_users.id de l'utilisateur à supprimer dans cette opération
|
||||
$stmtOldOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOldOpeUser->execute([$id, $operationId]);
|
||||
$oldOpeUserId = $stmtOldOpeUser->fetchColumn();
|
||||
|
||||
if (!$oldOpeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trouver ope_users.id de l'utilisateur de destination dans cette opération
|
||||
$stmtNewOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtNewOpeUser->execute([$transferTo, $operationId]);
|
||||
$newOpeUserId = $stmtNewOpeUser->fetchColumn();
|
||||
|
||||
if (!$newOpeUserId) {
|
||||
LogService::log('Impossible de transférer passages - utilisateur destination absent', [
|
||||
'level' => 'warning',
|
||||
'operation_id' => $operationId,
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transférer les passages
|
||||
$stmtTransfer = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_ope_user_id
|
||||
WHERE fk_user = :old_ope_user_id AND fk_operation = :operation_id
|
||||
');
|
||||
$stmtTransfer->execute([
|
||||
'new_ope_user_id' => $newOpeUserId,
|
||||
'old_ope_user_id' => $oldOpeUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
|
||||
$totalTransferred += $stmtTransfer->rowCount();
|
||||
}
|
||||
|
||||
LogService::log('Passages transférés avant suppression utilisateur', [
|
||||
'level' => 'info',
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo,
|
||||
'passages_transferred' => $transferredCount
|
||||
'operations_count' => count($operations),
|
||||
'passages_transferred' => $totalTransferred
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
Response::json([
|
||||
@@ -890,13 +948,10 @@ class UserController {
|
||||
// —— Suppression réelle de l'utilisateur ——
|
||||
try {
|
||||
// Supprimer les enregistrements dépendants dans ope_users
|
||||
// (CASCADE supprime automatiquement ope_users_sectors et ope_pass)
|
||||
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
|
||||
$stmtOpeUsers->execute([$id]);
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -908,12 +963,8 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector supprimé', [
|
||||
'level' => 'info',
|
||||
'deletedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
|
||||
]);
|
||||
// Log de suppression utilisateur (suppression physique = false pour soft_delete)
|
||||
EventLogService::logUserDeleted((int)$id, false);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class VilleController {
|
||||
|
||||
Reference in New Issue
Block a user