db = Database::getInstance(); $this->appConfig = AppConfig::getInstance(); } /** * Récupère l'entité de l'utilisateur connecté * * @param int $userId ID de l'utilisateur * @return int|null ID de l'entité ou null si non trouvé */ private function getUserEntiteId(int $userId): ?int { try { $stmt = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ? (int)$user['fk_entite'] : null; } catch (Exception $e) { LogService::log('Erreur lors de la récupération de l\'entité utilisateur', [ 'level' => 'error', 'error' => $e->getMessage(), 'userId' => $userId ]); return null; } } /** * Vérifie si l'utilisateur a accès à l'opération * * @param int $userId ID de l'utilisateur * @param int $operationId ID de l'opération * @return bool True si l'utilisateur a accès */ private function hasAccessToOperation(int $userId, int $operationId): bool { try { $entiteId = $this->getUserEntiteId($userId); if (!$entiteId) { return false; } $stmt = $this->db->prepare(' SELECT COUNT(*) as count FROM operations WHERE id = ? AND fk_entite = ? AND chk_active = 1 '); $stmt->execute([$operationId, $entiteId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result && $result['count'] > 0; } catch (Exception $e) { LogService::log('Erreur lors de la vérification d\'accès à l\'opération', [ 'level' => 'error', 'error' => $e->getMessage(), 'userId' => $userId, 'operationId' => $operationId ]); return false; } } /** * Valide les données d'un passage * * @param array $data Données à valider * @param int|null $passageId ID du passage (pour update) * @return array|null Erreurs de validation ou null */ private function validatePassageData(array $data, ?int $passageId = null): ?array { $errors = []; // Validation de l'opération if (!isset($data['fk_operation']) || empty($data['fk_operation'])) { $errors[] = 'L\'ID de l\'opération est obligatoire'; } // Validation de l'utilisateur if (!isset($data['fk_user']) || empty($data['fk_user'])) { $errors[] = 'L\'ID de l\'utilisateur est obligatoire'; } // Validation de l'adresse if (!isset($data['numero']) || empty(trim($data['numero']))) { $errors[] = 'Le numéro de rue est obligatoire'; } if (!isset($data['rue']) || empty(trim($data['rue']))) { $errors[] = 'Le nom de rue est obligatoire'; } if (!isset($data['ville']) || empty(trim($data['ville']))) { $errors[] = 'La ville est obligatoire'; } // Validation du nom (chiffré) - obligatoire seulement si (type=1 Effectué ou 5 Lot) ET email présent $fk_type = isset($data['fk_type']) ? (int)$data['fk_type'] : 0; $hasEmail = (isset($data['email']) && !empty(trim($data['email']))) || (isset($data['encrypted_email']) && !empty($data['encrypted_email'])); if (($fk_type === 1 || $fk_type === 5) && $hasEmail) { if (!isset($data['encrypted_name']) && !isset($data['name'])) { $errors[] = 'Le nom est obligatoire pour ce type de passage avec email'; } elseif (isset($data['name']) && empty(trim($data['name']))) { $errors[] = 'Le nom ne peut pas être vide pour ce type de passage avec email'; } } // Validation du montant if (isset($data['montant'])) { $montant = (float)$data['montant']; if ($montant < 0) { $errors[] = 'Le montant ne peut pas être négatif'; } if ($montant > 999999.99) { $errors[] = 'Le montant ne peut pas dépasser 999999.99'; } } // Validation de l'email si fourni if (isset($data['email']) && !empty($data['email'])) { if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors[] = 'Format d\'email invalide'; } } // Validation des coordonnées GPS si fournies if (isset($data['gps_lat']) && !empty($data['gps_lat'])) { $lat = (float)$data['gps_lat']; if ($lat < -90 || $lat > 90) { $errors[] = 'Latitude invalide (doit être entre -90 et 90)'; } } if (isset($data['gps_lng']) && !empty($data['gps_lng'])) { $lng = (float)$data['gps_lng']; if ($lng < -180 || $lng > 180) { $errors[] = 'Longitude invalide (doit être entre -180 et 180)'; } } // Validation de l'ID Stripe si fourni if (isset($data['stripe_payment_id']) && !empty($data['stripe_payment_id'])) { $stripeId = trim($data['stripe_payment_id']); // L'ID PaymentIntent Stripe doit commencer par 'pi_' if (!preg_match('/^pi_[a-zA-Z0-9]{24,}$/', $stripeId)) { $errors[] = 'Format d\'ID de paiement Stripe invalide'; } } return empty($errors) ? null : $errors; } /** * Récupère tous les passages de l'entité de l'utilisateur */ public function getPassages(): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $entiteId = $this->getUserEntiteId($userId); if (!$entiteId) { Response::json([ 'status' => 'error', 'message' => 'Entité non trouvée pour cet utilisateur' ], 404); return; } // Paramètres de pagination $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20; $offset = ($page - 1) * $limit; // Filtres optionnels $operationId = isset($_GET['operation_id']) ? (int)$_GET['operation_id'] : null; $userId_filter = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null; // Construction de la requête $whereConditions = ['o.fk_entite = ?']; $params = [$entiteId]; if ($operationId) { $whereConditions[] = 'p.fk_operation = ?'; $params[] = $operationId; } if ($userId_filter) { $whereConditions[] = 'p.fk_user = ?'; $params[] = $userId_filter; } $whereClause = implode(' AND ', $whereConditions); // Requête principale avec jointures $stmt = $this->db->prepare(" SELECT p.id, p.fk_operation, p.fk_sector, p.fk_user, p.fk_adresse, p.passed_at, p.fk_type, p.numero, p.rue, p.rue_bis, p.ville, 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_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 FROM ope_pass p INNER JOIN operations o ON p.fk_operation = o.id INNER JOIN users u ON p.fk_user = u.id WHERE $whereClause AND p.chk_active = 1 ORDER BY p.created_at DESC LIMIT ? OFFSET ? "); $params[] = $limit; $params[] = $offset; $stmt->execute($params); $passages = $stmt->fetchAll(PDO::FETCH_ASSOC); // Déchiffrement des données sensibles foreach ($passages as &$passage) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); $passage['email'] = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : ''; $passage['phone'] = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : ''; $passage['user_name'] = ApiService::decryptData($passage['user_name']); // Suppression des champs chiffrés unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']); } // Compter le total pour la pagination $countStmt = $this->db->prepare(" SELECT COUNT(*) as total FROM ope_pass p INNER JOIN operations o ON p.fk_operation = o.id WHERE $whereClause AND p.chk_active = 1 "); $countStmt->execute(array_slice($params, 0, -2)); // Enlever limit et offset $totalResult = $countStmt->fetch(PDO::FETCH_ASSOC); $total = $totalResult['total']; Response::json([ 'status' => 'success', 'passages' => $passages, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => $total, 'pages' => ceil($total / $limit) ] ], 200); } catch (Exception $e) { LogService::log('Erreur lors de la récupération des passages', [ 'level' => 'error', 'error' => $e->getMessage(), 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la récupération des passages' ], 500); } } /** * Récupère un passage spécifique par son ID */ public function getPassageById(string $id): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $entiteId = $this->getUserEntiteId($userId); if (!$entiteId) { Response::json([ 'status' => 'error', 'message' => 'Entité non trouvée pour cet utilisateur' ], 404); return; } $passageId = (int)$id; $stmt = $this->db->prepare(' SELECT p.*, o.libelle as operation_libelle, u.encrypted_name as user_name, u.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 WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1 '); $stmt->execute([$passageId, $entiteId]); $passage = $stmt->fetch(PDO::FETCH_ASSOC); if (!$passage) { Response::json([ 'status' => 'error', 'message' => 'Passage non trouvé' ], 404); return; } // Déchiffrement des données sensibles $passage['name'] = ApiService::decryptData($passage['encrypted_name']); $passage['email'] = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : ''; $passage['phone'] = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : ''; $passage['user_name'] = ApiService::decryptData($passage['user_name']); // Suppression des champs chiffrés unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']); Response::json([ 'status' => 'success', 'passage' => $passage ], 200); } catch (Exception $e) { LogService::log('Erreur lors de la récupération du passage', [ 'level' => 'error', 'error' => $e->getMessage(), 'passageId' => $id, 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la récupération du passage' ], 500); } } /** * Récupère tous les passages d'une opération spécifique */ public function getPassagesByOperation(string $operation_id): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $operationId = (int)$operation_id; // Vérifier l'accès à l'opération if (!$this->hasAccessToOperation($userId, $operationId)) { Response::json([ 'status' => 'error', 'message' => 'Vous n\'avez pas accès à cette opération' ], 403); return; } // Paramètres de pagination $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50; $offset = ($page - 1) * $limit; $stmt = $this->db->prepare(' SELECT 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.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 FROM ope_pass p INNER JOIN users u ON p.fk_user = u.id WHERE p.fk_operation = ? AND p.chk_active = 1 ORDER BY p.created_at DESC LIMIT ? OFFSET ? '); $stmt->execute([$operationId, $limit, $offset]); $passages = $stmt->fetchAll(PDO::FETCH_ASSOC); // Déchiffrement des données sensibles foreach ($passages as &$passage) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); $passage['email'] = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : ''; $passage['phone'] = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : ''; $passage['user_name'] = ApiService::decryptData($passage['user_name']); // Suppression des champs chiffrés unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']); } // Compter le total $countStmt = $this->db->prepare(' SELECT COUNT(*) as total FROM ope_pass WHERE fk_operation = ? AND chk_active = 1 '); $countStmt->execute([$operationId]); $totalResult = $countStmt->fetch(PDO::FETCH_ASSOC); $total = $totalResult['total']; Response::json([ 'status' => 'success', 'passages' => $passages, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => $total, 'pages' => ceil($total / $limit) ] ], 200); } catch (Exception $e) { LogService::log('Erreur lors de la récupération des passages par opération', [ 'level' => 'error', 'error' => $e->getMessage(), 'operationId' => $operation_id, 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la récupération des passages' ], 500); } } /** * Crée un nouveau passage */ public function createPassage(): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $data = Request::getJson(); // Validation des données $errors = $this->validatePassageData($data); if ($errors) { Response::json([ 'status' => 'error', 'message' => 'Erreurs de validation', 'errors' => $errors ], 400); return; } $operationId = (int)$data['fk_operation']; // Vérifier l'accès à l'opération if (!$this->hasAccessToOperation($userId, $operationId)) { Response::json([ 'status' => 'error', 'message' => 'Vous n\'avez pas accès à cette opération' ], 403); return; } // Chiffrement des données sensibles $encryptedName = ''; if (isset($data['name']) && !empty(trim($data['name']))) { $encryptedName = ApiService::encryptData($data['name']); } elseif (isset($data['encrypted_name']) && !empty($data['encrypted_name'])) { $encryptedName = $data['encrypted_name']; } // Le nom peut rester vide si les conditions ne l'exigent pas $encryptedEmail = isset($data['email']) && !empty($data['email']) ? ApiService::encryptSearchableData($data['email']) : ''; $encryptedPhone = isset($data['phone']) && !empty($data['phone']) ? ApiService::encryptData($data['phone']) : ''; // Préparation des données pour l'insertion $insertData = [ 'fk_operation' => $operationId, 'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0, 'fk_user' => (int)$data['fk_user'], '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, 'numero' => trim($data['numero']), 'rue' => trim($data['rue']), 'rue_bis' => $data['rue_bis'] ?? '', 'ville' => trim($data['ville']), 'fk_habitat' => isset($data['fk_habitat']) ? (int)$data['fk_habitat'] : 1, 'appt' => $data['appt'] ?? '', 'niveau' => $data['niveau'] ?? '', 'residence' => $data['residence'] ?? '', 'gps_lat' => $data['gps_lat'] ?? '', 'gps_lng' => $data['gps_lng'] ?? '', 'encrypted_name' => $encryptedName, 'montant' => isset($data['montant']) ? (float)$data['montant'] : 0.00, 'fk_type_reglement' => isset($data['fk_type_reglement']) ? (int)$data['fk_type_reglement'] : 1, 'remarque' => $data['remarque'] ?? '', 'encrypted_email' => $encryptedEmail, 'encrypted_phone' => $encryptedPhone, 'stripe_payment_id' => isset($data['stripe_payment_id']) ? trim($data['stripe_payment_id']) : null, 'nom_recu' => $data['nom_recu'] ?? null, 'date_recu' => isset($data['date_recu']) ? $data['date_recu'] : null, 'docremis' => isset($data['docremis']) ? (int)$data['docremis'] : 0, 'date_repasser' => isset($data['date_repasser']) ? $data['date_repasser'] : null, 'nb_passages' => isset($data['nb_passages']) ? (int)$data['nb_passages'] : 1, 'chk_mobile' => isset($data['chk_mobile']) ? (int)$data['chk_mobile'] : 0, 'anomalie' => isset($data['anomalie']) ? (int)$data['anomalie'] : 0, 'fk_user_creat' => $userId ]; // Construction de la requête d'insertion $fields = array_keys($insertData); $placeholders = array_fill(0, count($fields), '?'); $sql = 'INSERT INTO ope_pass (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; $stmt = $this->db->prepare($sql); $stmt->execute(array_values($insertData)); $passageId = $this->db->lastInsertId(); LogService::log('Création d\'un nouveau passage', [ 'level' => 'info', 'userId' => $userId, 'passageId' => $passageId, 'operationId' => $operationId ]); // Envoyer la réponse immédiatement pour éviter les timeouts Response::json([ 'status' => 'success', 'message' => 'Passage créé avec succès', 'passage_id' => $passageId, 'receipt_generated' => false // On va générer le reçu en arrière-plan ], 201); // Flush la sortie pour s'assurer que la réponse est envoyée if (ob_get_level()) { ob_end_flush(); } flush(); // Fermer la connexion HTTP mais continuer le traitement if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } // Générer automatiquement un reçu si c'est un don (fk_type = 1) avec email valide if (isset($data['fk_type']) && (int)$data['fk_type'] === 1) { // Vérifier si un email a été fourni $hasEmail = false; if (!empty($data['email'])) { $hasEmail = filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false; } elseif (!empty($encryptedEmail)) { // L'email a déjà été validé lors du chiffrement $hasEmail = true; } if ($hasEmail) { try { $receiptService = new \App\Services\ReceiptService(); $receiptGenerated = $receiptService->generateReceiptForPassage($passageId); if ($receiptGenerated) { LogService::log('Reçu généré automatiquement pour le passage', [ 'level' => 'info', 'passageId' => $passageId ]); } } catch (Exception $e) { LogService::log('Erreur lors de la génération automatique du reçu', [ 'level' => 'warning', 'error' => $e->getMessage(), 'passageId' => $passageId ]); } } } return; // Fin de la méthode, éviter d'exécuter le code après } catch (Exception $e) { LogService::log('Erreur lors de la création du passage', [ 'level' => 'error', 'error' => $e->getMessage(), 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la création du passage' ], 500); } } /** * Met à jour un passage existant */ public function updatePassage(string $id): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $passageId = (int)$id; $data = Request::getJson(); // Vérifier que le passage existe et appartient à l'entité de l'utilisateur $entiteId = $this->getUserEntiteId($userId); if (!$entiteId) { Response::json([ 'status' => 'error', 'message' => 'Entité non trouvée pour cet utilisateur' ], 404); return; } $stmt = $this->db->prepare(' SELECT p.id, p.fk_operation, p.fk_type, p.fk_user 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 '); $stmt->execute([$passageId, $entiteId]); $passage = $stmt->fetch(PDO::FETCH_ASSOC); if (!$passage) { Response::json([ 'status' => 'error', 'message' => 'Passage non trouvé' ], 404); return; } // Validation des données $errors = $this->validatePassageData($data, $passageId); if ($errors) { Response::json([ 'status' => 'error', 'message' => 'Erreurs de validation', 'errors' => $errors ], 400); 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; LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [ 'level' => 'info', 'passageId' => $passageId, 'ancien_user' => $passage['fk_user'], 'nouveau_user' => $userId ]); } // Construction de la requête de mise à jour dynamique $updateFields = []; $params = []; // Champs pouvant être mis à jour $updatableFields = [ 'fk_sector', 'fk_user', 'fk_adresse', 'passed_at', 'fk_type', 'numero', 'rue', 'rue_bis', 'ville', 'fk_habitat', 'appt', 'niveau', 'residence', 'gps_lat', 'gps_lng', 'montant', 'fk_type_reglement', 'remarque', 'stripe_payment_id', 'nom_recu', 'date_recu', 'docremis', 'date_repasser', 'nb_passages', 'chk_mobile', 'anomalie' ]; foreach ($updatableFields as $field) { if (isset($data[$field])) { $updateFields[] = "$field = ?"; $params[] = $data[$field]; } } // Gestion des champs chiffrés if (array_key_exists('name', $data)) { $updateFields[] = "encrypted_name = ?"; // Permettre de vider le nom si les conditions le permettent $params[] = !empty(trim($data['name'])) ? ApiService::encryptData($data['name']) : ''; } if (isset($data['email'])) { $updateFields[] = "encrypted_email = ?"; $params[] = !empty($data['email']) ? ApiService::encryptSearchableData($data['email']) : ''; } if (isset($data['phone'])) { $updateFields[] = "encrypted_phone = ?"; $params[] = !empty($data['phone']) ? ApiService::encryptData($data['phone']) : ''; } if (empty($updateFields)) { Response::json([ 'status' => 'error', 'message' => 'Aucune donnée à mettre à jour' ], 400); return; } // Ajout des champs de mise à jour $updateFields[] = "updated_at = NOW()"; $updateFields[] = "fk_user_modif = ?"; $params[] = $userId; $params[] = $passageId; $sql = 'UPDATE ope_pass SET ' . implode(', ', $updateFields) . ' WHERE id = ?'; $stmt = $this->db->prepare($sql); $stmt->execute($params); LogService::log('Mise à jour d\'un passage', [ 'level' => 'info', 'userId' => $userId, 'passageId' => $passageId ]); // Envoyer la réponse immédiatement pour éviter les timeouts Response::json([ 'status' => 'success', 'message' => 'Passage mis à jour avec succès', 'receipt_generated' => false // On va générer le reçu en arrière-plan ], 200); // Flush la sortie pour s'assurer que la réponse est envoyée if (ob_get_level()) { ob_end_flush(); } flush(); // Fermer la connexion HTTP mais continuer le traitement if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } // Maintenant générer le reçu en arrière-plan après avoir envoyé la réponse try { // Récupérer les données actualisées du passage $stmt = $this->db->prepare('SELECT fk_type, encrypted_email, nom_recu FROM ope_pass WHERE id = ?'); $stmt->execute([$passageId]); $updatedPassage = $stmt->fetch(PDO::FETCH_ASSOC); if ($updatedPassage) { // Générer un reçu si : // - C'est un don (fk_type = 1) // - Il y a un email valide // - Il n'y a pas encore de reçu (nom_recu est vide ou null) if ((int)$updatedPassage['fk_type'] === 1 && !empty($updatedPassage['encrypted_email']) && empty($updatedPassage['nom_recu'])) { // Vérifier que l'email est valide en le déchiffrant $email = ApiService::decryptSearchableData($updatedPassage['encrypted_email']); if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) { $receiptService = new \App\Services\ReceiptService(); $receiptGenerated = $receiptService->generateReceiptForPassage($passageId); if ($receiptGenerated) { LogService::log('Reçu généré automatiquement après mise à jour du passage', [ 'level' => 'info', 'passageId' => $passageId ]); } } } } } catch (Exception $e) { LogService::log('Erreur lors de la génération automatique du reçu après mise à jour', [ 'level' => 'warning', 'error' => $e->getMessage(), 'passageId' => $passageId ]); } return; // Fin de la méthode, éviter d'exécuter le code après } catch (Exception $e) { LogService::log('Erreur lors de la mise à jour du passage', [ 'level' => 'error', 'error' => $e->getMessage(), 'passageId' => $id, 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la mise à jour du passage' ], 500); } } /** * Supprime (désactive) un passage */ public function deletePassage(string $id): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $passageId = (int)$id; // Récupérer le rôle de l'utilisateur $stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { Response::json([ 'status' => 'error', 'message' => 'Utilisateur non trouvé' ], 404); return; } $userRole = (int)$user['fk_role']; $entiteId = (int)$user['fk_entite']; // Si l'utilisateur est un membre (fk_role = 1), vérifier les permissions de l'entité if ($userRole === 1) { $stmt = $this->db->prepare('SELECT chk_user_delete_pass FROM entites WHERE id = ?'); $stmt->execute([$entiteId]); $entite = $stmt->fetch(PDO::FETCH_ASSOC); if (!$entite || (int)$entite['chk_user_delete_pass'] !== 1) { LogService::log('Tentative de suppression de passage non autorisée', [ 'level' => 'warning', 'userId' => $userId, 'userRole' => $userRole, 'entiteId' => $entiteId, 'passageId' => $passageId, 'chk_user_delete_pass' => $entite ? $entite['chk_user_delete_pass'] : null ]); Response::json([ 'status' => 'error', 'message' => 'Vous n\'avez pas l\'autorisation de supprimer des passages' ], 403); return; } } // Vérifier que le passage existe et appartient à l'entité de l'utilisateur if (!$entiteId) { Response::json([ 'status' => 'error', 'message' => 'Entité non trouvée pour cet utilisateur' ], 404); return; } $stmt = $this->db->prepare(' SELECT p.id 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 '); $stmt->execute([$passageId, $entiteId]); $passage = $stmt->fetch(PDO::FETCH_ASSOC); if (!$passage) { Response::json([ 'status' => 'error', 'message' => 'Passage non trouvé' ], 404); return; } // Désactiver le passage (soft delete) $stmt = $this->db->prepare(' 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 ]); Response::json([ 'status' => 'success', 'message' => 'Passage supprimé avec succès' ], 200); } catch (Exception $e) { LogService::log('Erreur lors de la suppression du passage', [ 'level' => 'error', 'error' => $e->getMessage(), 'passageId' => $id, 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la suppression du passage' ], 500); } } /** * Récupère le reçu PDF d'un passage * * @param string $id ID du passage * @return void */ public function getReceipt(string $id): void { try { $userId = Session::getUserId(); if (!$userId) { Response::json([ 'status' => 'error', 'message' => 'Vous devez être connecté pour effectuer cette action' ], 401); return; } $passageId = (int)$id; // Vérifier que le passage existe et que l'utilisateur y a accès $entiteId = $this->getUserEntiteId($userId); if (!$entiteId) { Response::json([ 'status' => 'error', 'message' => 'Entité non trouvée pour cet utilisateur' ], 404); return; } // Récupérer les informations du passage et du reçu $stmt = $this->db->prepare(' SELECT p.id, p.nom_recu, p.date_creat_recu, p.fk_operation, o.fk_entite 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 '); $stmt->execute([$passageId, $entiteId]); $passage = $stmt->fetch(PDO::FETCH_ASSOC); if (!$passage) { Response::json([ 'status' => 'error', 'message' => 'Passage non trouvé ou accès non autorisé' ], 404); return; } if (empty($passage['nom_recu'])) { Response::json([ 'status' => 'error', 'message' => 'Aucun reçu disponible pour ce passage' ], 404); return; } // Récupérer le fichier depuis la table medias $stmt = $this->db->prepare(' SELECT file_path, mime_type, file_size, fichier FROM medias WHERE support = ? AND support_id = ? AND file_category = ? ORDER BY created_at DESC LIMIT 1 '); $stmt->execute(['passage', $passageId, 'recu']); $media = $stmt->fetch(PDO::FETCH_ASSOC); if (!$media) { // Si pas trouvé dans medias, essayer de construire le chemin $filePath = __DIR__ . '/../../uploads/entites/' . $passage['fk_entite'] . '/recus/' . $passage['fk_operation'] . '/' . $passage['nom_recu']; if (!file_exists($filePath)) { Response::json([ 'status' => 'error', 'message' => 'Fichier reçu introuvable' ], 404); return; } $media = [ 'file_path' => $filePath, 'mime_type' => 'application/pdf', 'fichier' => $passage['nom_recu'], 'file_size' => filesize($filePath) ]; } // Vérifier que le fichier existe if (!file_exists($media['file_path'])) { Response::json([ 'status' => 'error', 'message' => 'Fichier reçu introuvable sur le serveur' ], 404); return; } // Lire le contenu du fichier $pdfContent = file_get_contents($media['file_path']); if ($pdfContent === false) { Response::json([ 'status' => 'error', 'message' => 'Impossible de lire le fichier reçu' ], 500); return; } // Option 1: Retourner le PDF directement (pour téléchargement) if (isset($_GET['download']) && $_GET['download'] === 'true') { header('Content-Type: ' . $media['mime_type']); header('Content-Disposition: attachment; filename="' . $media['fichier'] . '"'); header('Content-Length: ' . $media['file_size']); header('Cache-Control: no-cache, must-revalidate'); echo $pdfContent; exit; } // Option 2: Retourner le PDF en base64 dans JSON (pour Flutter) $base64 = base64_encode($pdfContent); Response::json([ 'status' => 'success', 'receipt' => [ 'passage_id' => $passageId, 'file_name' => $media['fichier'], 'mime_type' => $media['mime_type'], 'file_size' => $media['file_size'], 'created_at' => $passage['date_creat_recu'], 'data_base64' => $base64 ] ], 200); } catch (Exception $e) { LogService::log('Erreur lors de la récupération du reçu', [ 'level' => 'error', 'error' => $e->getMessage(), 'passageId' => $id, 'userId' => $userId ?? null ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la récupération du reçu' ], 500); } } }