Livraison d ela gestion des opérations v0.4.0

This commit is contained in:
d6soft
2025-06-24 13:01:43 +02:00
parent 25c9d5874c
commit 416d648a14
813 changed files with 234012 additions and 73933 deletions

View File

@@ -0,0 +1,803 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.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é)
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
$errors[] = 'Le nom est obligatoire';
}
// 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)';
}
}
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.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.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 = isset($data['name']) ? ApiService::encryptData($data['name']) : (isset($data['encrypted_name']) ? $data['encrypted_name'] : '');
$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,
'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
]);
Response::json([
'status' => 'success',
'message' => 'Passage créé avec succès',
'passage_id' => $passageId
], 201);
} 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
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;
}
// 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',
'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 (isset($data['name'])) {
$updateFields[] = "encrypted_name = ?";
$params[] = 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
]);
Response::json([
'status' => 'success',
'message' => 'Passage mis à jour avec succès'
], 200);
} 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;
// 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
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);
}
}
}