- 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>
1131 lines
44 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|
|
}
|