Files
geo/api/src/Controllers/PassageController.php
pierre 570a1fa1f0 feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:11:15 +02:00

1131 lines
44 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Controllers;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/../Services/ReceiptService.php';
use PDO;
use PDOException;
use Database;
use AppConfig;
use Request;
use Response;
use Session;
use LogService;
use ApiService;
use Exception;
use DateTime;
class PassageController {
private PDO $db;
private AppConfig $appConfig;
public function __construct() {
$this->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);
}
}
}